Branch data Line data Source code
1 : : /*
2 : : * Copyright (C) 2026 by Thun Lu. All rights reserved.
3 : : * Author: Thun Lu <thun.lu@zohomail.cn>
4 : : * Repo: https://github.com/thun-res/vlink
5 : : * _ __ __ _ __
6 : : * | | / / / / (_) ____ / /__
7 : : * | | / / / / / / / __ \ / //_/
8 : : * | |/ / / /___ / / / / / / / ,<
9 : : * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10 : : *
11 : : * Licensed under the Apache License, Version 2.0 (the "License");
12 : : * you may not use this file except in compliance with the License.
13 : : * You may obtain a copy of the License at
14 : : *
15 : : * http://www.apache.org/licenses/LICENSE-2.0
16 : : *
17 : : * Unless required by applicable law or agreed to in writing, software
18 : : * distributed under the License is distributed on an "AS IS" BASIS,
19 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 : : * See the License for the specific language governing permissions and
21 : : * limitations under the License.
22 : : */
23 : :
24 : : #include "./extension/trigger_recorder.h"
25 : :
26 : : #include <algorithm>
27 : : #include <atomic>
28 : : #include <cctype>
29 : : #include <chrono>
30 : : #include <cstdint>
31 : : #include <deque>
32 : : #include <exception>
33 : : #include <filesystem>
34 : : #include <limits>
35 : : #include <memory>
36 : : #include <mutex>
37 : : #include <shared_mutex>
38 : : #include <string>
39 : : #include <string_view>
40 : : #include <system_error>
41 : : #include <thread>
42 : : #include <unordered_map>
43 : : #include <unordered_set>
44 : : #include <utility>
45 : : #include <vector>
46 : :
47 : : #include "./base/elapsed_timer.h"
48 : : #include "./base/helpers.h"
49 : : #include "./base/logger.h"
50 : : #include "./base/timer.h"
51 : : #include "./base/utils.h"
52 : : #include "./extension/bag_writer.h"
53 : : #include "./extension/trigger_plugin_interface.h"
54 : :
55 : : namespace vlink {
56 : :
57 : : // TriggerRecorder::UrlBuffer
58 : : struct TriggerRecorder::UrlBuffer final {
59 : : struct Entry final {
60 : : int64_t capture_ts_us{0};
61 : : std::shared_ptr<const Bytes> payload;
62 : : };
63 : :
64 : : std::mutex mtx;
65 : : std::deque<Entry> ring;
66 : : int64_t bytes{0};
67 : : int64_t pre_us{0};
68 : : int64_t post_us{0};
69 : : std::atomic<int64_t> retention_us{0};
70 : : int64_t max_packet_size{0};
71 : : int64_t max_size{0};
72 : :
73 : : std::string url;
74 : : std::string ser_type;
75 : : SampleLostInfo last_lost;
76 : : SampleLostInfo final_lost;
77 : : SchemaType schema_type{SchemaType::kUnknown};
78 : : bool frozen{false};
79 : : bool disabled{false};
80 : : bool getter_semantics{false};
81 : :
82 : : std::shared_ptr<RawSub> sub;
83 : : };
84 : :
85 : : // TriggerRecorder::DumpJob
86 : : struct TriggerRecorder::DumpJob final {
87 : : int64_t trigger_ts{0};
88 : : TriggerParams params;
89 : : std::string path;
90 : : std::vector<std::shared_ptr<UrlBuffer>> frozen_buffers;
91 : : };
92 : :
93 : : // TriggerRecorder::Impl
94 : : struct TriggerRecorder::Impl final { // NOLINT(clang-analyzer-optin.performance.Padding)
95 : : Config config;
96 : : RawSubFactory raw_sub_factory;
97 : : ElapsedTimer capture_timer{ElapsedTimer::kCpuTimestamp, ElapsedTimer::kMicro};
98 : : int64_t anchor_wall_us{0};
99 : :
100 : : std::atomic<int64_t> max_post_all_us{0};
101 : : std::atomic<int64_t> global_bytes{0};
102 : : std::atomic_bool cache_overflow_warned{false};
103 : : std::atomic_bool dumping{false};
104 : : std::atomic_bool writing{false};
105 : : std::atomic_bool running{false};
106 : :
107 : : std::mutex lifecycle_mtx;
108 : : std::shared_mutex url_buffer_mtx;
109 : :
110 : : std::unordered_map<std::string, std::shared_ptr<UrlBuffer>> url_buffer_map;
111 : : std::shared_ptr<DumpJob> active_dump_job;
112 : :
113 : : std::unique_ptr<DiscoveryViewer> viewer;
114 : : Timer sweep_timer;
115 : :
116 : : std::shared_ptr<TriggerPluginInterface> trigger_plugin;
117 : : std::shared_ptr<BagPluginInterface> bag_plugin;
118 : : };
119 : :
120 : : // TriggerRecorder
121 [ + - ]: 30 : TriggerRecorder::TriggerRecorder(const Config& config, RawSubFactory&& factory) : impl_(std::make_unique<Impl>()) {
122 [ + - + - ]: 30 : set_name("TriggerRecorder");
123 : :
124 [ + - ]: 30 : impl_->config = config;
125 : 30 : impl_->raw_sub_factory = std::move(factory);
126 : :
127 [ + + ]: 30 : if VUNLIKELY (!impl_->raw_sub_factory) {
128 [ + - + - : 2 : VLOG_F("TriggerRecorder: raw subscriber factory is not set");
- + ]
129 : : }
130 : :
131 [ + + ]: 29 : if (impl_->config.dump_dir.empty()) {
132 [ + - ]: 6 : impl_->config.dump_dir = Utils::get_tmp_dir() + "/vlink-trigger";
133 : : }
134 : :
135 : 29 : impl_->config.default_pre_ms = std::max<int64_t>(0, impl_->config.default_pre_ms);
136 : 29 : impl_->config.default_post_ms = std::max<int64_t>(0, impl_->config.default_post_ms);
137 : 29 : impl_->config.retention_guard_ms = std::max<int64_t>(0, impl_->config.retention_guard_ms);
138 : :
139 [ + + - + : 29 : if VUNLIKELY (impl_->config.default_pre_ms > kMaxWindowMs || impl_->config.default_post_ms > kMaxWindowMs ||
+ + - + +
+ ]
140 : : impl_->config.retention_guard_ms > kMaxWindowMs) {
141 [ + - + - : 2 : VLOG_F("TriggerRecorder: pre/post/retention_guard exceeds the supported range");
- + ]
142 : : }
143 : :
144 [ - + ]: 28 : if VUNLIKELY (impl_->config.default_post_ms + impl_->config.retention_guard_ms >
145 : : std::numeric_limits<uint32_t>::max()) {
146 [ # # # # : 0 : VLOG_F("TriggerRecorder: default post window plus retention guard exceeds the timer range");
# # ]
147 : : }
148 : :
149 [ + - - + : 28 : if VUNLIKELY (impl_->config.default_max_packet_size < 0 || impl_->config.default_max_size < 0 ||
+ - + + +
+ - + + +
- + + + ]
150 : : impl_->config.max_cache_size < 0 || impl_->config.sleep_interval < 0 ||
151 : : impl_->config.sleep_time_ms < 0) {
152 [ + - + - : 2 : VLOG_F("TriggerRecorder: byte limits and sleep values must be non-negative");
- + ]
153 : : }
154 : :
155 [ + + - + : 27 : if VUNLIKELY (impl_->config.file_type != kVdb && impl_->config.file_type != kVcap) {
- + ]
156 [ # # # # : 0 : VLOG_F("TriggerRecorder: unsupported file type");
# # ]
157 : : }
158 : :
159 [ + - - + : 27 : if VUNLIKELY (impl_->config.overflow != kCoverOldest && impl_->config.overflow != kDropNewest) {
- + ]
160 [ # # # # : 0 : VLOG_F("TriggerRecorder: unsupported overflow policy");
# # ]
161 : : }
162 : :
163 [ - + ]: 27 : for (const auto& [url, value] : impl_->config.url_overrides) {
164 [ # # # # : 0 : if VUNLIKELY (value.pre_ms > kMaxWindowMs || value.post_ms > kMaxWindowMs ||
# # # # #
# # # #
# ]
165 : : (value.post_ms >= 0 &&
166 : : value.post_ms + impl_->config.retention_guard_ms > std::numeric_limits<uint32_t>::max())) {
167 [ # # # # : 0 : VLOG_F("TriggerRecorder: URL window exceeds the supported range: ", url);
# # ]
168 : : }
169 : : }
170 : :
171 : 27 : std::error_code ec;
172 [ + - + - ]: 27 : std::filesystem::create_directories(impl_->config.dump_dir, ec);
173 : :
174 : 27 : std::error_code query_ec;
175 : :
176 [ + + + - : 27 : if VUNLIKELY (ec && !std::filesystem::is_directory(impl_->config.dump_dir, query_ec)) {
+ - + + +
+ - - ]
177 : : VLOG_F("TriggerRecorder: cannot create dump_dir '", // LCOV_EXCL_LINE GCOVR_EXCL_LINE
178 : : impl_->config.dump_dir, "': ", ec.message()); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
179 : : }
180 : :
181 [ + - ]: 26 : impl_->viewer = std::make_unique<DiscoveryViewer>(impl_->config.discovery_filter);
182 [ + - + - ]: 43 : impl_->viewer->register_callback([this](const std::vector<DiscoveryViewer::Info>& list) { handle_discovery(list); });
183 : 34 : }
184 : :
185 : 26 : TriggerRecorder::~TriggerRecorder() {
186 : 26 : quit(true);
187 : 26 : wait_for_quit();
188 : 26 : }
189 : :
190 : 23 : bool TriggerRecorder::dump(const TriggerParams& params) {
191 : 23 : std::string out_file;
192 : :
193 [ + - ]: 46 : return dump(params, out_file);
194 : 23 : }
195 : :
196 : 27 : bool TriggerRecorder::dump(const TriggerParams& params, std::string& out_file) {
197 [ + + - + : 27 : if VUNLIKELY (params.pre_ms > kMaxWindowMs || params.post_ms > kMaxWindowMs) {
+ + ]
198 : 1 : out_file.clear();
199 [ + - + - : 2 : VLOG_E("TriggerRecorder: trigger window exceeds the supported range");
+ - ]
200 : 1 : return false;
201 : : }
202 : :
203 [ + - ]: 26 : std::lock_guard lifecycle_lock(impl_->lifecycle_mtx);
204 : :
205 [ + + + + : 26 : if VUNLIKELY (!impl_->running.load(std::memory_order_acquire) || impl_->dumping.load(std::memory_order_acquire)) {
+ + ]
206 : 3 : out_file.clear();
207 : 3 : return false;
208 : : }
209 : :
210 [ + - ]: 23 : auto job = std::make_shared<DumpJob>();
211 [ + - ]: 23 : job->params = params;
212 : 23 : const auto& request = job->params;
213 : 23 : out_file.clear();
214 : :
215 [ + + ]: 23 : if (!request.out_file.empty()) {
216 [ + - ]: 12 : job->path = request.out_file;
217 : : } else {
218 [ + + ]: 11 : const char* file_suffix = impl_->config.file_type == kVcap ? ".vcap" : ".vdb";
219 [ + + + - : 11 : std::string base = request.name_hint.empty() ? BagWriter::get_format_date(nullptr, true) : request.name_hint;
+ - ]
220 : 11 : std::replace(base.begin(), base.end(), '/', '_');
221 : 11 : std::replace(base.begin(), base.end(), '\\', '_');
222 [ + - + - : 11 : job->path = impl_->config.dump_dir + "/" + base + file_suffix;
+ - ]
223 : 11 : uint64_t suffix = 1;
224 : 11 : std::error_code ec;
225 : :
226 [ + - + + ]: 12 : while (std::filesystem::exists(job->path, ec)) {
227 [ + - + - : 1 : job->path = impl_->config.dump_dir + "/" + base + "_" + std::to_string(suffix++) + file_suffix;
+ - + - +
- + - ]
228 : : }
229 : 11 : }
230 : :
231 : 23 : int64_t max_post = 0;
232 : 23 : std::vector<std::string> filter_list = Helpers::split_any(request.filter_str);
233 : :
234 : : {
235 [ + - ]: 23 : std::shared_lock map_lock(impl_->url_buffer_mtx);
236 [ + - ]: 23 : job->frozen_buffers.reserve(impl_->url_buffer_map.size());
237 : 23 : job->trigger_ts = impl_->capture_timer.get();
238 [ + + ]: 43 : for (const auto& entry : impl_->url_buffer_map) {
239 [ - + ]: 20 : if (entry.second->disabled) {
240 : 2 : continue;
241 : : }
242 : :
243 [ + + + - : 39 : if ((!request.whitelist.empty() && request.whitelist.count(entry.second->url) == 0) ||
+ + + + ]
244 [ + - + + ]: 19 : request.blacklist.count(entry.second->url) != 0) {
245 : 2 : continue;
246 : : }
247 : :
248 [ - + ]: 18 : if (!filter_list.empty()) {
249 [ # # ]: 0 : bool skip = request.black_mode ? false : true;
250 : :
251 [ # # ]: 0 : std::string left_str = entry.second->url;
252 : 0 : std::transform(left_str.begin(), left_str.end(), left_str.begin(),
253 : 0 : [](unsigned char c) { return std::tolower(c); });
254 [ # # ]: 0 : for (const auto& filter : filter_list) {
255 [ # # ]: 0 : if (filter.empty()) {
256 : 0 : continue;
257 : : }
258 : :
259 [ # # ]: 0 : std::string right_str = filter;
260 : 0 : std::transform(right_str.begin(), right_str.end(), right_str.begin(),
261 : 0 : [](unsigned char c) { return std::tolower(c); });
262 : :
263 [ # # ]: 0 : if (left_str.find(right_str) != std::string::npos) {
264 [ # # ]: 0 : skip = request.black_mode ? true : false;
265 : 0 : break;
266 : : }
267 [ # # ]: 0 : }
268 : :
269 [ # # ]: 0 : if (skip) {
270 : 0 : continue;
271 : : }
272 [ # # ]: 0 : }
273 : :
274 : 18 : entry.second->frozen = true;
275 : : job->frozen_buffers.push_back(entry.second); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
276 : :
277 : : const int64_t post =
278 [ - + ]: 18 : request.post_ms >= 0 ? std::min(request.post_ms * 1000, entry.second->post_us) : entry.second->post_us;
279 : 18 : max_post = std::max(max_post, post);
280 : : }
281 : :
282 : 23 : impl_->active_dump_job = job;
283 : 23 : impl_->dumping.store(true, std::memory_order_release);
284 : 23 : }
285 : :
286 [ + + ]: 23 : const int64_t delay_ms = max_post > 0 ? max_post / 1000 + impl_->config.retention_guard_ms : 0;
287 : :
288 : 23 : std::weak_ptr<DumpJob> weak_job = job;
289 : 43 : auto task = [this, weak_job]() {
290 [ + + ]: 22 : if (auto locked_job = weak_job.lock()) {
291 [ + - ]: 21 : do_dump(*locked_job);
292 : 22 : }
293 : 45 : };
294 : :
295 [ + - + - : 46 : VLOG_I("TriggerRecorder: dump scheduling -> ", job->path, " wait_ms=", delay_ms,
+ - ]
296 : : " urls=", job->frozen_buffers.size());
297 : :
298 [ + + + - : 26 : const bool ok = delay_ms > 0 ? Timer::call_once(this, static_cast<uint32_t>(delay_ms), std::move(task))
+ - - - ]
299 [ + - + - : 23 : : post_task(std::move(task));
+ + + + -
- ]
300 : :
301 [ + + ]: 23 : if VUNLIKELY (!ok) {
302 : : // LCOV_EXCL_START GCOVR_EXCL_START
303 : : finish_dump_locked(*job);
304 : :
305 : : return false;
306 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
307 : : }
308 : :
309 [ + - ]: 22 : out_file = job->path;
310 : :
311 : 22 : return true;
312 : 26 : }
313 : :
314 : 386 : bool TriggerRecorder::is_dumping() const noexcept { return impl_->dumping.load(std::memory_order_acquire); }
315 : :
316 : 10 : void TriggerRecorder::bind_trigger_interface(const std::shared_ptr<TriggerPluginInterface>& trigger_interface) {
317 [ + - ]: 10 : std::lock_guard lifecycle_lock(impl_->lifecycle_mtx);
318 : :
319 [ - + ]: 10 : if VUNLIKELY (impl_->running.load(std::memory_order_acquire)) {
320 [ # # # # : 0 : VLOG_E("TriggerRecorder: trigger plugin must be bound while the recorder is stopped");
# # ]
321 : 0 : return;
322 : : }
323 : :
324 : 10 : impl_->trigger_plugin = trigger_interface;
325 [ + - ]: 10 : }
326 : :
327 [ # # ]: 0 : void TriggerRecorder::clear_trigger_interface() { bind_trigger_interface(nullptr); }
328 : :
329 : 8 : void TriggerRecorder::bind_bag_interface(const std::shared_ptr<BagPluginInterface>& bag_interface) {
330 [ + - ]: 8 : std::lock_guard lock(impl_->lifecycle_mtx);
331 : :
332 [ - + ]: 8 : if VUNLIKELY (impl_->running.load(std::memory_order_acquire)) {
333 [ # # # # : 0 : VLOG_E("TriggerRecorder: bag plugin must be bound while the recorder is stopped");
# # ]
334 : 0 : return;
335 : : }
336 : :
337 : 8 : impl_->bag_plugin = bag_interface;
338 [ + - ]: 8 : }
339 : :
340 [ + - ]: 1 : void TriggerRecorder::clear_bag_interface() { bind_bag_interface(nullptr); }
341 : :
342 : 24 : void TriggerRecorder::on_begin() {
343 [ + - ]: 24 : MessageLoop::on_begin();
344 : :
345 [ + - ]: 24 : std::lock_guard lifecycle_lock(impl_->lifecycle_mtx);
346 : :
347 [ + + ]: 24 : if (!impl_->viewer) {
348 : : try {
349 [ + - ]: 2 : impl_->viewer = std::make_unique<DiscoveryViewer>(impl_->config.discovery_filter);
350 [ + - + - ]: 2 : impl_->viewer->register_callback(
351 : 1 : [this](const std::vector<DiscoveryViewer::Info>& list) { handle_discovery(list); });
352 [ - - ]: 0 : } catch (const std::exception& e) {
353 [ # # # # : 0 : VLOG_E("TriggerRecorder: discovery viewer setup failed: ", e.what());
# # ]
354 : 0 : return;
355 : 0 : }
356 : : }
357 : :
358 : 24 : impl_->capture_timer.start();
359 : 24 : impl_->cache_overflow_warned.store(false, std::memory_order_relaxed);
360 : 48 : impl_->anchor_wall_us =
361 : 24 : static_cast<int64_t>(ElapsedTimer::get_sys_timestamp(ElapsedTimer::kMicro)) - impl_->capture_timer.get();
362 : :
363 [ + - - + ]: 24 : if VUNLIKELY (!impl_->viewer->async_run()) {
364 : : VLOG_E("TriggerRecorder: discovery viewer failed to start"); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
365 : : return; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
366 : : }
367 : :
368 [ + - - + ]: 24 : if VUNLIKELY (!impl_->sweep_timer.attach(this)) {
369 : 0 : return;
370 : : }
371 : :
372 [ + - + - ]: 26 : impl_->sweep_timer.start([this]() { sweep_evict(); });
373 : :
374 : 24 : impl_->running.store(true, std::memory_order_release);
375 : :
376 [ + + ]: 24 : if (impl_->trigger_plugin) {
377 [ + - ]: 11 : impl_->trigger_plugin->on_start();
378 : : }
379 [ + - ]: 24 : }
380 : :
381 : 24 : void TriggerRecorder::on_end() {
382 [ + - ]: 24 : std::unique_lock lifecycle_lock(impl_->lifecycle_mtx);
383 : :
384 : 24 : const bool recorder_started = impl_->running.exchange(false, std::memory_order_acq_rel);
385 [ + - ]: 24 : impl_->sweep_timer.stop();
386 : :
387 [ + - ]: 24 : if (impl_->viewer) {
388 [ + - ]: 24 : impl_->viewer->quit(true);
389 [ + - ]: 24 : impl_->viewer->wait_for_quit();
390 : 24 : impl_->viewer.reset();
391 : : }
392 : :
393 : 24 : impl_->dumping.store(false, std::memory_order_release);
394 : 24 : auto abandoned_job = std::move(impl_->active_dump_job);
395 : :
396 : 24 : std::vector<std::shared_ptr<RawSub>> retired_subscribers;
397 : :
398 : : {
399 [ + - ]: 24 : std::unique_lock lock(impl_->url_buffer_mtx);
400 [ + - ]: 24 : retired_subscribers.reserve(impl_->url_buffer_map.size());
401 : :
402 [ + + ]: 45 : for (auto& entry : impl_->url_buffer_map) {
403 [ + - + - ]: 21 : retired_subscribers.push_back(deactivate_url_buffer(*entry.second));
404 : : }
405 : :
406 : 24 : impl_->url_buffer_map.clear();
407 : 24 : impl_->global_bytes.store(0, std::memory_order_relaxed);
408 : 24 : impl_->max_post_all_us.store(0, std::memory_order_relaxed);
409 : 24 : }
410 : :
411 : 24 : retired_subscribers.clear();
412 : :
413 [ + - ]: 24 : if (recorder_started) {
414 [ + + ]: 24 : if VUNLIKELY (abandoned_job) {
415 [ + - ]: 1 : notify_dump_failed(*abandoned_job, "dump abandoned at shutdown");
416 : : }
417 : :
418 [ + + ]: 24 : if (impl_->trigger_plugin) {
419 [ + - ]: 11 : impl_->trigger_plugin->flush();
420 [ + - ]: 11 : impl_->trigger_plugin->on_stop();
421 : : }
422 : : }
423 : :
424 [ + - ]: 24 : lifecycle_lock.unlock();
425 : :
426 [ + - ]: 24 : MessageLoop::on_end();
427 : 24 : }
428 : :
429 : : // LCOV_EXCL_START GCOVR_EXCL_START
430 : : void TriggerRecorder::handle_data(UrlBuffer& url_buffer, const Bytes& data) {
431 : : if VUNLIKELY (!impl_->running.load(std::memory_order_relaxed) || url_buffer.disabled) {
432 : : return;
433 : : }
434 : :
435 : : if VUNLIKELY (impl_->config.busy_skip_data && impl_->writing.load(std::memory_order_relaxed)) {
436 : : return;
437 : : }
438 : :
439 : : const size_t data_size = data.size();
440 : :
441 : : if VUNLIKELY (data_size > static_cast<size_t>(std::numeric_limits<int64_t>::max())) {
442 : : return;
443 : : }
444 : :
445 : : const auto incoming = static_cast<int64_t>(data_size);
446 : :
447 : : if VUNLIKELY (url_buffer.max_packet_size > 0 && incoming > url_buffer.max_packet_size) {
448 : : return;
449 : : }
450 : :
451 : : if VUNLIKELY (url_buffer.max_size > 0 && incoming > url_buffer.max_size) {
452 : : return;
453 : : }
454 : :
455 : : if VUNLIKELY (impl_->config.max_cache_size > 0 && incoming > impl_->config.max_cache_size) {
456 : : if (!impl_->cache_overflow_warned.exchange(true, std::memory_order_relaxed)) {
457 : : VLOG_W("TriggerRecorder: max_cache_size exceeded, current=", impl_->global_bytes.load(std::memory_order_relaxed),
458 : : " incoming=", incoming, " limit=", impl_->config.max_cache_size, " action=drop");
459 : : }
460 : :
461 : : return;
462 : : }
463 : :
464 : : auto payload = std::make_shared<const Bytes>(Bytes::deep_copy(data.data(), data_size));
465 : :
466 : : if VUNLIKELY (payload->size() != data_size) {
467 : : return;
468 : : }
469 : :
470 : : std::lock_guard lock(url_buffer.mtx);
471 : :
472 : : if VUNLIKELY (!url_buffer.sub) {
473 : : return;
474 : : }
475 : :
476 : : const int64_t capture_ts = impl_->capture_timer.get();
477 : : const int64_t horizon = capture_ts - url_buffer.retention_us.load(std::memory_order_relaxed);
478 : :
479 : : while (!url_buffer.ring.empty() && url_buffer.ring.front().capture_ts_us < horizon) {
480 : : const auto size = static_cast<int64_t>(url_buffer.ring.front().payload->size());
481 : : url_buffer.bytes -= size;
482 : : impl_->global_bytes.fetch_sub(size, std::memory_order_relaxed);
483 : : url_buffer.ring.pop_front();
484 : : }
485 : :
486 : : const size_t evictable_count = url_buffer.ring.size();
487 : : size_t evict_count = 0;
488 : : int64_t evict_bytes = 0;
489 : : int64_t local_need = 0;
490 : :
491 : : if (url_buffer.max_size > 0 && url_buffer.bytes > url_buffer.max_size - incoming) {
492 : : local_need = url_buffer.bytes - (url_buffer.max_size - incoming);
493 : : }
494 : :
495 : : if (local_need > 0 && impl_->config.overflow == kDropNewest) {
496 : : return;
497 : : }
498 : :
499 : : url_buffer.ring.push_back(UrlBuffer::Entry{capture_ts, std::move(payload)});
500 : :
501 : : int64_t global = impl_->global_bytes.load(std::memory_order_relaxed);
502 : :
503 : : while (true) {
504 : : int64_t global_need = 0;
505 : :
506 : : if (impl_->config.max_cache_size > 0 && global > impl_->config.max_cache_size - incoming) {
507 : : global_need = global - (impl_->config.max_cache_size - incoming);
508 : :
509 : : if (!impl_->cache_overflow_warned.exchange(true, std::memory_order_relaxed)) {
510 : : VLOG_W("TriggerRecorder: max_cache_size exceeded, current=", global, " incoming=", incoming,
511 : : " limit=", impl_->config.max_cache_size,
512 : : " policy=", impl_->config.overflow == kDropNewest ? "drop" : "cover");
513 : : }
514 : :
515 : : if (impl_->config.overflow == kDropNewest) {
516 : : url_buffer.ring.pop_back();
517 : : return;
518 : : }
519 : : }
520 : :
521 : : const int64_t need = std::max(local_need, global_need);
522 : :
523 : : while (evict_count < evictable_count && evict_bytes < need) {
524 : : evict_bytes += static_cast<int64_t>(url_buffer.ring[evict_count].payload->size());
525 : : ++evict_count;
526 : : }
527 : :
528 : : while (evict_count > 0 &&
529 : : evict_bytes - static_cast<int64_t>(url_buffer.ring[evict_count - 1].payload->size()) >= need) {
530 : : evict_bytes -= static_cast<int64_t>(url_buffer.ring[evict_count - 1].payload->size());
531 : : --evict_count;
532 : : }
533 : :
534 : : if (evict_bytes < need || evict_bytes > global) {
535 : : url_buffer.ring.pop_back();
536 : : return;
537 : : }
538 : :
539 : : if VUNLIKELY (global - evict_bytes > std::numeric_limits<int64_t>::max() - incoming) {
540 : : url_buffer.ring.pop_back();
541 : : return;
542 : : }
543 : :
544 : : const int64_t desired = global - evict_bytes + incoming;
545 : :
546 : : if (impl_->global_bytes.compare_exchange_weak(global, desired, std::memory_order_relaxed,
547 : : std::memory_order_relaxed)) {
548 : : break;
549 : : }
550 : : }
551 : :
552 : : for (size_t index = 0; index < evict_count; ++index) {
553 : : url_buffer.ring.pop_front();
554 : : }
555 : :
556 : : url_buffer.bytes = url_buffer.bytes - evict_bytes + incoming;
557 : : }
558 : :
559 : : std::shared_ptr<TriggerRecorder::UrlBuffer> TriggerRecorder::build_url_buffer(const DiscoveryViewer::Info& info) {
560 : : auto url_buffer = std::make_shared<UrlBuffer>();
561 : : url_buffer->url = info.url;
562 : : url_buffer->ser_type = info.ser_type;
563 : : url_buffer->schema_type = info.schema_type;
564 : :
565 : : UrlConfig url_config;
566 : :
567 : : auto override_iter = impl_->config.url_overrides.find(info.url);
568 : :
569 : : if (override_iter != impl_->config.url_overrides.end()) {
570 : : url_config = override_iter->second;
571 : : }
572 : :
573 : : const int64_t pre_ms = url_config.pre_ms >= 0 ? url_config.pre_ms : impl_->config.default_pre_ms;
574 : : const int64_t post_ms = url_config.post_ms >= 0 ? url_config.post_ms : impl_->config.default_post_ms;
575 : :
576 : : url_buffer->pre_us = url_config.only_back ? 0 : pre_ms * 1000;
577 : : url_buffer->post_us = url_config.only_front ? 0 : post_ms * 1000;
578 : : url_buffer->disabled = url_config.only_front && url_config.only_back;
579 : : url_buffer->getter_semantics = (info.type & kSetter) != 0;
580 : : url_buffer->max_packet_size =
581 : : url_config.max_packet_size >= 0 ? url_config.max_packet_size : impl_->config.default_max_packet_size;
582 : : url_buffer->max_size = url_config.max_size >= 0 ? url_config.max_size : impl_->config.default_max_size;
583 : :
584 : : const int64_t guard = impl_->config.retention_guard_ms * 1000;
585 : : const int64_t initial_max_post =
586 : : std::max(impl_->max_post_all_us.load(std::memory_order_relaxed), url_buffer->post_us);
587 : : url_buffer->retention_us.store(url_buffer->pre_us + initial_max_post + 2 * guard, std::memory_order_relaxed);
588 : :
589 : : if (url_buffer->disabled) {
590 : : VLOG_W("TriggerRecorder: URL has both only_front and only_back, window is empty: ", info.url);
591 : : }
592 : :
593 : : try {
594 : : auto sub = impl_->raw_sub_factory(info.url, InitType::kWithoutInit);
595 : :
596 : : if VUNLIKELY (!sub) {
597 : : VLOG_W("TriggerRecorder: raw subscriber factory returned null, URL skipped: ", info.url);
598 : : return nullptr;
599 : : }
600 : :
601 : : if (url_buffer->getter_semantics) {
602 : : sub->mark_as_getter();
603 : : }
604 : :
605 : : sub->set_latency_and_lost_enabled(true);
606 : : sub->set_ser_type(info.ser_type, info.schema_type);
607 : : sub->set_discovery_enabled(false);
608 : : sub->set_safety_quit(true);
609 : :
610 : : if VUNLIKELY (!sub->init()) {
611 : : VLOG_W("TriggerRecorder: subscriber init failed, URL skipped: ", info.url);
612 : : return nullptr;
613 : : }
614 : :
615 : : url_buffer->sub = std::move(sub);
616 : : std::weak_ptr<UrlBuffer> weak_buffer = url_buffer;
617 : :
618 : : if VUNLIKELY (!url_buffer->sub->listen([this, weak_buffer](const Bytes& data) {
619 : : if (auto locked = weak_buffer.lock()) {
620 : : handle_data(*locked, data);
621 : : }
622 : : })) {
623 : : deactivate_url_buffer(*url_buffer);
624 : : VLOG_W("TriggerRecorder: subscriber listen failed, URL skipped: ", info.url);
625 : : return nullptr;
626 : : }
627 : : } catch (const std::exception& e) {
628 : : if (url_buffer->sub) {
629 : : deactivate_url_buffer(*url_buffer);
630 : : }
631 : :
632 : : VLOG_W("TriggerRecorder: subscriber setup failed, URL skipped: ", info.url, " (", e.what(), ")");
633 : : return nullptr;
634 : : }
635 : :
636 : : return url_buffer;
637 : : }
638 : :
639 : : std::shared_ptr<TriggerRecorder::RawSub> TriggerRecorder::deactivate_url_buffer(UrlBuffer& url_buffer) {
640 : : std::lock_guard ring_lock(url_buffer.mtx);
641 : :
642 : : if (url_buffer.frozen) {
643 : : url_buffer.final_lost = url_buffer.sub->get_lost();
644 : : } else if (url_buffer.bytes > 0) {
645 : : impl_->global_bytes.fetch_sub(url_buffer.bytes, std::memory_order_relaxed);
646 : : }
647 : :
648 : : return std::move(url_buffer.sub);
649 : : }
650 : :
651 : : void TriggerRecorder::recompute_retention() {
652 : : int64_t max_post = 0;
653 : :
654 : : for (const auto& entry : impl_->url_buffer_map) {
655 : : max_post = std::max(max_post, entry.second->post_us);
656 : : }
657 : :
658 : : impl_->max_post_all_us.store(max_post, std::memory_order_relaxed);
659 : :
660 : : const int64_t guard = impl_->config.retention_guard_ms * 1000;
661 : :
662 : : for (const auto& entry : impl_->url_buffer_map) {
663 : : UrlBuffer* url_buffer = entry.second.get();
664 : : const int64_t updated = url_buffer->pre_us + max_post + 2 * guard;
665 : :
666 : : if (url_buffer->frozen && updated < url_buffer->retention_us.load(std::memory_order_relaxed)) {
667 : : continue;
668 : : }
669 : :
670 : : url_buffer->retention_us.store(updated, std::memory_order_relaxed);
671 : : }
672 : : }
673 : :
674 : : void TriggerRecorder::handle_discovery(const std::vector<DiscoveryViewer::Info>& list) {
675 : : if VUNLIKELY (!impl_->running.load(std::memory_order_acquire)) {
676 : : return;
677 : : }
678 : :
679 : : std::vector<std::shared_ptr<UrlBuffer>> new_buffers;
680 : : std::unordered_set<std::string_view> current;
681 : :
682 : : if (impl_->config.destroy_on_offline) {
683 : : current.reserve(list.size());
684 : : }
685 : :
686 : : for (const auto& info : list) {
687 : : if (impl_->config.destroy_on_offline) {
688 : : current.insert(info.url);
689 : : }
690 : :
691 : : if ((info.type & (kPublisher | kSetter)) == 0) {
692 : : continue;
693 : : }
694 : :
695 : : if ((std::find(impl_->config.blacklist.begin(), impl_->config.blacklist.end(), info.url) !=
696 : : impl_->config.blacklist.end()) ||
697 : : (!impl_->config.whitelist.empty() && std::find(impl_->config.whitelist.begin(), impl_->config.whitelist.end(),
698 : : info.url) == impl_->config.whitelist.end())) {
699 : : continue;
700 : : }
701 : :
702 : : auto existing = impl_->url_buffer_map.find(info.url);
703 : :
704 : : if VLIKELY (existing != impl_->url_buffer_map.end()) {
705 : : if VLIKELY (existing->second->ser_type == info.ser_type && existing->second->schema_type == info.schema_type &&
706 : : existing->second->getter_semantics == ((info.type & kSetter) != 0)) {
707 : : continue;
708 : : }
709 : : }
710 : :
711 : : auto url_buffer = build_url_buffer(info);
712 : :
713 : : if (url_buffer) {
714 : : new_buffers.push_back(std::move(url_buffer));
715 : : }
716 : : }
717 : :
718 : : if (new_buffers.empty() && !impl_->config.destroy_on_offline) {
719 : : return;
720 : : }
721 : :
722 : : std::vector<std::shared_ptr<RawSub>> retired_subscribers;
723 : :
724 : : {
725 : : std::unique_lock lock(impl_->url_buffer_mtx);
726 : : bool retention_changed = false;
727 : :
728 : : for (auto& url_buffer : new_buffers) {
729 : : std::string url = url_buffer->url;
730 : : auto existing = impl_->url_buffer_map.find(url);
731 : :
732 : : if (existing == impl_->url_buffer_map.end()) {
733 : : impl_->url_buffer_map.emplace(std::move(url), std::move(url_buffer));
734 : : retention_changed = true;
735 : : continue;
736 : : }
737 : :
738 : : retired_subscribers.push_back(deactivate_url_buffer(*existing->second));
739 : : existing->second = std::move(url_buffer);
740 : : }
741 : :
742 : : if (impl_->config.destroy_on_offline) {
743 : : for (auto iter = impl_->url_buffer_map.begin(); iter != impl_->url_buffer_map.end();) {
744 : : if VUNLIKELY (current.count(std::string_view(iter->first)) == 0) {
745 : : retired_subscribers.push_back(deactivate_url_buffer(*iter->second));
746 : : iter = impl_->url_buffer_map.erase(iter);
747 : : retention_changed = true;
748 : : } else {
749 : : ++iter;
750 : : }
751 : : }
752 : : }
753 : :
754 : : if (retention_changed) {
755 : : recompute_retention();
756 : : }
757 : : }
758 : : }
759 : :
760 : : void TriggerRecorder::sweep_evict() {
761 : : const int64_t now = impl_->capture_timer.get();
762 : :
763 : : std::shared_lock lock(impl_->url_buffer_mtx);
764 : :
765 : : for (const auto& entry : impl_->url_buffer_map) {
766 : : UrlBuffer* url_buffer = entry.second.get();
767 : :
768 : : std::lock_guard ring_lock(url_buffer->mtx);
769 : :
770 : : const int64_t horizon = now - url_buffer->retention_us.load(std::memory_order_relaxed);
771 : :
772 : : while (!url_buffer->ring.empty() && url_buffer->ring.front().capture_ts_us < horizon) {
773 : : const auto size = static_cast<int64_t>(url_buffer->ring.front().payload->size());
774 : : url_buffer->bytes -= size;
775 : : impl_->global_bytes.fetch_sub(size, std::memory_order_relaxed);
776 : : url_buffer->ring.pop_front();
777 : : }
778 : : }
779 : : }
780 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
781 : :
782 : 21 : void TriggerRecorder::finish_dump(DumpJob& job) {
783 [ + - ]: 21 : std::lock_guard lifecycle_lock(impl_->lifecycle_mtx);
784 [ + - ]: 21 : finish_dump_locked(job);
785 : 21 : }
786 : :
787 : 22 : void TriggerRecorder::finish_dump_locked(DumpJob& job) {
788 [ - + ]: 22 : if VUNLIKELY (impl_->active_dump_job.get() != &job) {
789 : 0 : return;
790 : : }
791 : :
792 [ + - ]: 22 : std::shared_lock map_lock(impl_->url_buffer_mtx);
793 : :
794 [ + + ]: 38 : for (const auto& url_buffer : job.frozen_buffers) {
795 [ + - ]: 16 : std::lock_guard ring_lock(url_buffer->mtx);
796 : 16 : url_buffer->frozen = false;
797 : :
798 [ - + - - : 16 : if (!url_buffer->sub && url_buffer->bytes > 0) {
- + ]
799 : 0 : impl_->global_bytes.fetch_sub(url_buffer->bytes, std::memory_order_relaxed);
800 : : }
801 : 16 : }
802 : :
803 : 22 : impl_->writing.store(false, std::memory_order_release);
804 [ + - ]: 22 : recompute_retention();
805 : 22 : impl_->dumping.store(false, std::memory_order_release);
806 : 22 : impl_->active_dump_job.reset();
807 : 22 : }
808 : :
809 : : // LCOV_EXCL_START GCOVR_EXCL_START
810 : : void TriggerRecorder::notify_dump_failed(const DumpJob& job, std::string_view error) {
811 : : VLOG_E("TriggerRecorder: dump failed -> ", job.path, " error=", error);
812 : :
813 : : if (!impl_->trigger_plugin) {
814 : : return;
815 : : }
816 : :
817 : : TriggerPluginInterface::DumpResult result;
818 : : result.reason = job.params.reason;
819 : : result.path = job.path;
820 : : result.error = error;
821 : : impl_->trigger_plugin->on_dump_failed(result);
822 : : }
823 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
824 : :
825 : 21 : void TriggerRecorder::do_dump(DumpJob& job) {
826 : : struct FinishGuard final {
827 : : TriggerRecorder& recorder;
828 : : DumpJob& job;
829 : :
830 : 21 : ~FinishGuard() { recorder.finish_dump(job); }
831 : : };
832 : :
833 : : struct SnapFrame final {
834 : : int64_t capture_ts_us;
835 : : std::shared_ptr<const Bytes> payload;
836 : : const UrlBuffer* source;
837 : : };
838 : :
839 : : struct LossInfo final {
840 : : const UrlBuffer* source;
841 : : double loss;
842 : : };
843 : :
844 : 21 : FinishGuard guard{*this, job};
845 : :
846 : 21 : const auto& params = job.params;
847 : 21 : const auto& path = job.path;
848 : 21 : auto* trigger_plugin = impl_->trigger_plugin.get();
849 : 21 : const int64_t trigger_ts = job.trigger_ts;
850 : 21 : const int64_t dump_start = impl_->capture_timer.get();
851 : :
852 [ - + ]: 21 : const int64_t trigger_pre_us = params.pre_ms >= 0 ? params.pre_ms * 1000 : -1;
853 [ - + ]: 21 : const int64_t trigger_post_us = params.post_ms >= 0 ? params.post_ms * 1000 : -1;
854 : :
855 : 21 : std::vector<SnapFrame> snapshot;
856 : 21 : std::vector<LossInfo> losses;
857 [ + - ]: 21 : losses.reserve(job.frozen_buffers.size());
858 : :
859 : : // LCOV_EXCL_START GCOVR_EXCL_START
860 : : for (const auto& url_buffer : job.frozen_buffers) {
861 : : std::lock_guard ring_lock(url_buffer->mtx);
862 : : SampleLostInfo current_lost = url_buffer->sub ? url_buffer->sub->get_lost() : url_buffer->final_lost;
863 : : const uint64_t delta_total = current_lost.total - url_buffer->last_lost.total;
864 : : const uint64_t delta_lost = current_lost.lost - url_buffer->last_lost.lost;
865 : : const double loss = delta_total > 0 ? static_cast<double>(delta_lost) / static_cast<double>(delta_total) : 0.0;
866 : : url_buffer->last_lost = current_lost;
867 : :
868 : : const int64_t pre = trigger_pre_us >= 0 ? std::min(trigger_pre_us, url_buffer->pre_us) : url_buffer->pre_us;
869 : : const int64_t post = trigger_post_us >= 0 ? std::min(trigger_post_us, url_buffer->post_us) : url_buffer->post_us;
870 : :
871 : : const int64_t window_begin = trigger_ts - pre;
872 : : const int64_t window_end = trigger_ts + post;
873 : :
874 : : auto first = std::lower_bound(
875 : : url_buffer->ring.begin(), url_buffer->ring.end(), window_begin,
876 : : [](const UrlBuffer::Entry& entry, int64_t timestamp) { return entry.capture_ts_us < timestamp; });
877 : :
878 : : for (; first != url_buffer->ring.end() && first->capture_ts_us <= window_end; ++first) {
879 : : snapshot.push_back(SnapFrame{first->capture_ts_us, first->payload, url_buffer.get()});
880 : : }
881 : :
882 : : losses.push_back(LossInfo{url_buffer.get(), loss});
883 : : }
884 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
885 : :
886 [ + + ]: 21 : if (trigger_plugin) {
887 : 14 : TriggerPluginInterface::TriggerContext context;
888 [ + - ]: 14 : context.reason = params.reason;
889 [ + - ]: 14 : context.name_hint = params.name_hint;
890 [ + - ]: 14 : context.out_file = params.out_file;
891 : 14 : context.pre_ms = params.pre_ms;
892 : 14 : context.post_ms = params.post_ms;
893 : 14 : context.trigger_timestamp = impl_->anchor_wall_us + trigger_ts;
894 : :
895 [ + - ]: 14 : trigger_plugin->on_trigger(context);
896 : 14 : }
897 : :
898 [ + + + - : 21 : if (params.out_file.empty() && impl_->config.max_dump_file_count > 0) {
+ + ]
899 [ + - + - ]: 10 : const auto file_extension = std::filesystem::path(path).extension();
900 : 10 : std::vector<std::string> removed;
901 : 10 : std::error_code ec;
902 : 10 : std::vector<std::filesystem::directory_entry> files;
903 [ + - + - ]: 10 : std::filesystem::directory_iterator iter(impl_->config.dump_dir, ec);
904 : :
905 [ + - ]: 10 : if (!ec) {
906 [ + - + + ]: 16 : for (const std::filesystem::directory_iterator end; iter != end; iter.increment(ec)) {
907 : 6 : std::error_code type_ec;
908 : :
909 [ + - + - : 6 : if (iter->is_regular_file(type_ec) && iter->path().extension() == file_extension) {
+ - + - +
- - - ]
910 [ + - ]: 6 : files.push_back(*iter);
911 : : }
912 : 10 : }
913 : :
914 [ + - ]: 10 : std::sort(files.begin(), files.end(),
915 : 3 : [](const std::filesystem::directory_entry& left, const std::filesystem::directory_entry& right) {
916 : 3 : std::error_code left_error;
917 : 3 : std::error_code right_error;
918 [ + - ]: 3 : return std::filesystem::last_write_time(left, left_error) <
919 : 6 : std::filesystem::last_write_time(right, right_error);
920 : : });
921 : :
922 : 10 : const auto max_count = static_cast<size_t>(impl_->config.max_dump_file_count);
923 [ + + ]: 10 : const size_t remove_count = files.size() >= max_count ? files.size() - max_count + 1 : 0;
924 : :
925 [ + + ]: 12 : for (size_t index = 0; index < remove_count; ++index) {
926 : 2 : std::error_code remove_error;
927 : :
928 [ + - + - : 2 : if (std::filesystem::remove(files[index].path(), remove_error) && trigger_plugin) {
+ - ]
929 [ + - + - ]: 2 : removed.push_back(files[index].path().string());
930 : : }
931 : : }
932 : : }
933 : :
934 [ + + ]: 10 : if (trigger_plugin) {
935 [ + + ]: 11 : for (const auto& removed_path : removed) {
936 [ + - ]: 2 : trigger_plugin->on_file_rotated(removed_path);
937 : : }
938 : : }
939 : 10 : }
940 : :
941 [ + - ]: 21 : std::stable_sort(snapshot.begin(), snapshot.end(), [](const SnapFrame& left, const SnapFrame& right) {
942 : : return left.capture_ts_us < right.capture_ts_us; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
943 : : });
944 : :
945 [ + + ]: 21 : const int64_t min_capture = snapshot.empty() ? trigger_ts : std::min(trigger_ts, snapshot.front().capture_ts_us);
946 : :
947 : 21 : BagWriter::Config writer_config;
948 [ - + ]: 21 : writer_config.compress = impl_->config.enable_compress ? BagWriter::kCompressAuto : BagWriter::kCompressNone;
949 : 21 : writer_config.enable_chunk_crc = impl_->config.enable_chunk_crc;
950 [ + - ]: 21 : writer_config.tag_name = params.reason;
951 : 21 : writer_config.sync_mode = true;
952 : 21 : writer_config.optimize_on_exit = true;
953 : 21 : writer_config.start_timestamp = (impl_->anchor_wall_us + min_capture) / 1000;
954 : :
955 : 21 : std::shared_ptr<BagWriter> writer;
956 : :
957 : : try {
958 [ + - ]: 21 : writer = BagWriter::create(path, writer_config);
959 [ - - ]: 0 : } catch (const std::exception& e) {
960 : : // LCOV_EXCL_START GCOVR_EXCL_START
961 : : notify_dump_failed(job, e.what());
962 : : return;
963 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
964 : 0 : }
965 : :
966 [ - + ]: 21 : if VUNLIKELY (!writer) {
967 : : // LCOV_EXCL_START GCOVR_EXCL_START
968 : : notify_dump_failed(job, "unsupported bag suffix");
969 : : return;
970 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
971 : : }
972 : :
973 [ + + ]: 21 : if (impl_->bag_plugin) {
974 [ + - ]: 5 : writer->bind_bag_interface(impl_->bag_plugin);
975 : : }
976 : :
977 [ + + ]: 36 : for (const auto& info : losses) {
978 : : writer->set_url_loss(info.source->url, info.loss); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
979 : : }
980 : :
981 : 21 : TriggerPluginInterface::DumpContext dump_context;
982 : :
983 : 21 : impl_->writing.store(true, std::memory_order_release);
984 [ + - + - : 42 : VLOG_I("TriggerRecorder: dump writing -> ", path, " frames=", snapshot.size(), " urls=", losses.size());
+ - ]
985 : :
986 [ + + ]: 21 : if (trigger_plugin) {
987 [ + - ]: 14 : dump_context.reason = params.reason;
988 [ + - ]: 14 : dump_context.path = path;
989 : 14 : dump_context.start_timestamp = writer_config.start_timestamp;
990 : 14 : dump_context.url_count = static_cast<int64_t>(losses.size());
991 [ + - ]: 14 : trigger_plugin->on_dump_started(dump_context);
992 : : }
993 : :
994 : 21 : int64_t throttle_bytes = 0;
995 : 21 : int64_t byte_count = 0;
996 : 21 : bool persistence_failed = false;
997 : 21 : const size_t snapshot_frame_count = snapshot.size();
998 : 21 : Frame frame;
999 : :
1000 [ + + ]: 143 : for (auto& item : snapshot) {
1001 : : // LCOV_EXCL_START GCOVR_EXCL_START
1002 : : const auto item_size = static_cast<int64_t>(item.payload->size());
1003 : : frame.timestamp = item.capture_ts_us - min_capture;
1004 : : frame.url = item.source->url;
1005 : : frame.ser_type = item.source->ser_type;
1006 : : frame.schema_type = item.source->schema_type;
1007 : : frame.action_type = item.source->getter_semantics ? ActionType::kGet : ActionType::kSubscribe;
1008 : : frame.data = Bytes::shallow_copy(item.payload->data(), item.payload->size());
1009 : :
1010 : : if VUNLIKELY (writer->push(frame) < 0) {
1011 : : frame.data.clear();
1012 : : item.payload.reset();
1013 : : persistence_failed = true;
1014 : : break;
1015 : : }
1016 : :
1017 : : byte_count += item_size;
1018 : :
1019 : : if (trigger_plugin) {
1020 : : trigger_plugin->on_frame(frame, dump_context);
1021 : : }
1022 : :
1023 : : frame.data.clear();
1024 : : item.payload.reset();
1025 : :
1026 : : if (impl_->config.sleep_time_ms > 0 && impl_->config.sleep_interval > 0) {
1027 : : throttle_bytes += item_size;
1028 : :
1029 : : if (throttle_bytes >= impl_->config.sleep_interval) {
1030 : : std::this_thread::sleep_for(std::chrono::milliseconds(impl_->config.sleep_time_ms));
1031 : : throttle_bytes = 0;
1032 : : }
1033 : : }
1034 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
1035 : : }
1036 : :
1037 : 21 : frame.data.clear();
1038 : :
1039 : 21 : std::vector<SnapFrame>().swap(snapshot);
1040 : :
1041 [ + + ]: 21 : if (impl_->bag_plugin) {
1042 [ + - ]: 5 : writer->clear_bag_interface();
1043 : : }
1044 : :
1045 [ + - ]: 21 : writer->close();
1046 : 21 : impl_->writing.store(false, std::memory_order_release);
1047 : :
1048 : 21 : const bool writer_failed = writer->fail();
1049 : :
1050 : 21 : writer.reset();
1051 : :
1052 [ + - - + : 21 : if VUNLIKELY (persistence_failed || writer_failed) {
- + ]
1053 [ # # ]: 0 : notify_dump_failed(job, "writer persistence failure");
1054 : 0 : return;
1055 : : }
1056 : :
1057 [ + + ]: 21 : if (trigger_plugin) {
1058 : 14 : TriggerPluginInterface::DumpResult result;
1059 [ + - ]: 14 : result.reason = params.reason;
1060 [ + - ]: 14 : result.path = path;
1061 : 14 : result.frame_count = static_cast<int64_t>(snapshot_frame_count);
1062 : 14 : result.byte_count = byte_count;
1063 : 14 : result.url_count = static_cast<int64_t>(losses.size());
1064 : 14 : result.start_timestamp = writer_config.start_timestamp;
1065 : 14 : result.duration_us = impl_->capture_timer.get() - dump_start;
1066 : 14 : result.success = true;
1067 : :
1068 [ + - ]: 14 : trigger_plugin->on_dump_finished(result);
1069 : 14 : }
1070 : :
1071 [ + - + - : 42 : VLOG_I("TriggerRecorder: dump finished -> ", path, " frames=", snapshot_frame_count, " bytes=", byte_count);
+ - ]
1072 [ + - + - : 21 : }
+ - + - +
- + - +
- ]
1073 : :
1074 : : } // namespace vlink
|