1 // Copyright (C) 2019 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #include "compiler/compiler.h"
16 #include "maintenance/controller.h"
17
18 #include "common/cmd_utils.h"
19 #include "common/debug.h"
20 #include "common/expected.h"
21 #include "common/trace.h"
22
23 #include "db/models.h"
24 #include "inode2filename/inode.h"
25 #include "inode2filename/search_directories.h"
26 #include "prefetcher/read_ahead.h"
27
28 #include <android-base/file.h>
29 #include <utils/Printer.h>
30
31 #include <chrono>
32 #include <ctime>
33 #include <iostream>
34 #include <filesystem>
35 #include <fstream>
36 #include <limits>
37 #include <mutex>
38 #include <optional>
39 #include <vector>
40 #include <string>
41 #include <sys/wait.h>
42
43 namespace iorap::maintenance {
44
45 const constexpr int64_t kCompilerCheckIntervalMs = 10;
46 static constexpr size_t kMinTracesForCompilation = 1;
47 const constexpr char* kDenyListFilterDexFiles = "[.](art|oat|odex|vdex|dex)$";
48
49 struct LastJobInfo {
50 time_t last_run_ns_{0};
51 size_t activities_last_compiled_{0};
52 };
53
54 LastJobInfo last_job_info_;
55 std::mutex last_job_info_mutex_;
56
57 // Gets the path of output compiled trace.
CalculateNewestFilePath(const std::string & package_name,const std::string & activity_name,int version)58 db::CompiledTraceFileModel CalculateNewestFilePath(
59 const std::string& package_name,
60 const std::string& activity_name,
61 int version) {
62 db::VersionedComponentName versioned_component_name{
63 package_name, activity_name, version};
64
65 db::CompiledTraceFileModel output_file =
66 db::CompiledTraceFileModel::CalculateNewestFilePath(versioned_component_name);
67
68 return output_file;
69 }
70
71 using ArgString = const char*;
72
73 static constexpr const char kCommandFileName[] = "/system/bin/iorap.cmd.compiler";
74
Execve(const std::string & pathname,std::vector<std::string> & argv_vec,char * const envp[])75 int Exec::Execve(const std::string& pathname,
76 std::vector<std::string>& argv_vec,
77 char *const envp[]) {
78 std::unique_ptr<ArgString[]> argv_ptr =
79 common::VecToArgv(kCommandFileName, argv_vec);
80
81 return execve(pathname.c_str(), (char**)argv_ptr.get(), envp);
82 }
83
Fork()84 pid_t Exec::Fork() {
85 return fork();
86 }
87
88 // Represents the parameters used when fork+exec compiler.
89 struct CompilerForkParameters {
90 std::vector<std::string> input_pbs;
91 std::vector<uint64_t> timestamp_limit_ns;
92 std::string output_proto;
93 std::vector<int32_t> pids;
94 ControllerParameters controller_params;
95
CompilerForkParametersiorap::maintenance::CompilerForkParameters96 CompilerForkParameters(const std::vector<compiler::CompilationInput>& perfetto_traces,
97 const std::string& output_proto,
98 ControllerParameters controller_params) :
99 output_proto(output_proto), controller_params(controller_params) {
100 for (compiler::CompilationInput perfetto_trace : perfetto_traces) {
101 input_pbs.push_back(perfetto_trace.filename);
102 timestamp_limit_ns.push_back(perfetto_trace.timestamp_limit_ns);
103 pids.push_back(perfetto_trace.pid);
104 }
105 }
106 };
107
MakeCompilerParams(const CompilerForkParameters & params)108 std::vector<std::string> MakeCompilerParams(const CompilerForkParameters& params) {
109 std::vector<std::string> argv;
110 ControllerParameters controller_params = params.controller_params;
111
112 common::AppendArgsRepeatedly(argv, params.input_pbs);
113 common::AppendArgsRepeatedly(argv, "--timestamp_limit_ns", params.timestamp_limit_ns);
114 common::AppendArgsRepeatedly(argv, "--pid", params.pids);
115
116 if (controller_params.output_text) {
117 argv.push_back("--output-text");
118 }
119
120 common::AppendArgs(argv, "--output-proto", params.output_proto);
121
122 if (controller_params.inode_textcache) {
123 common::AppendArgs(argv, "--inode-textcache", *controller_params.inode_textcache);
124 }
125
126 if (controller_params.verbose) {
127 argv.push_back("--verbose");
128 }
129
130 if (controller_params.exclude_dex_files) {
131 common::AppendArgs(argv, "--denylist-filter", kDenyListFilterDexFiles);
132 }
133
134 return argv;
135 }
136
137 // Sets a watch dog for the given pid and kill it if timeout.
SetTimeoutWatchDog(pid_t pid,int64_t timeout_ms,std::atomic<bool> & cancel_watchdog)138 std::thread SetTimeoutWatchDog(pid_t pid, int64_t timeout_ms, std::atomic<bool>& cancel_watchdog) {
139 std::thread watchdog_thread{[pid, timeout_ms, &cancel_watchdog]() {
140 std::chrono::time_point start = std::chrono::system_clock::now();
141 std::chrono::milliseconds timeout(timeout_ms);
142 while (!cancel_watchdog) {
143 int status = kill(pid, 0);
144 if (status != 0) {
145 LOG(DEBUG) << "Process (" << pid << ") doesn't exist now.";
146 break;
147 }
148 std::chrono::time_point cur = std::chrono::system_clock::now();
149 if (cur - start > timeout) {
150 LOG(INFO) << "Process (" << pid << ") is timeout!";
151 LOG(INFO) << "start time: "
152 << std::chrono::system_clock::to_time_t(start)
153 << " end time: "
154 << std::chrono::system_clock::to_time_t(cur)
155 << " timeout: "
156 << timeout_ms;
157 kill(pid, SIGKILL);
158 break;
159 }
160 usleep(kCompilerCheckIntervalMs * 1000);
161 }
162 }};
163
164 return watchdog_thread;
165 }
166
StartViaFork(const CompilerForkParameters & params)167 bool StartViaFork(const CompilerForkParameters& params) {
168 const ControllerParameters& controller_params = params.controller_params;
169 pid_t child = controller_params.exec->Fork();
170
171 if (child == -1) {
172 LOG(FATAL) << "Failed to fork a process for compilation";
173 } else if (child > 0) { // we are the caller of this function
174 LOG(DEBUG) << "forked into a process for compilation , pid = " << child;
175
176 int64_t compiler_timeout_ms =
177 android::base::GetIntProperty("iorapd.maintenance.compiler_timeout_ms",
178 /*default*/ 10 * 60 * 1000); // 10 min
179 std::atomic<bool> cancel_watchdog(false);
180 std::thread watchdog_thread = SetTimeoutWatchDog(child, compiler_timeout_ms, cancel_watchdog);
181 int wstatus;
182 waitpid(child, /*out*/&wstatus, /*options*/0);
183
184 // Terminate the thread after the compiler process is killed or done.
185 LOG(DEBUG) << "Terminate the watch dog thread.";
186 cancel_watchdog = true;
187 watchdog_thread.join();
188
189 if (!WIFEXITED(wstatus)) {
190 LOG(ERROR) << "Child terminated abnormally, status: " << WEXITSTATUS(wstatus);
191 return false;
192 }
193
194 int status = WEXITSTATUS(wstatus);
195 LOG(DEBUG) << "Child terminated, status: " << status;
196 if (status == 0) {
197 LOG(DEBUG) << "Iorap compilation succeeded";
198 return true;
199 } else {
200 LOG(ERROR) << "Iorap compilation failed";
201 return false;
202 }
203 } else {
204 // we are the child that was forked.
205 std::vector<std::string> argv_vec = MakeCompilerParams(params);
206 std::unique_ptr<ArgString[]> argv_ptr =
207 common::VecToArgv(kCommandFileName, argv_vec);
208
209 std::stringstream argv; // for debugging.
210 for (std::string arg : argv_vec) {
211 argv << arg << ' ';
212 }
213 LOG(DEBUG) << "fork+exec: " << kCommandFileName << " " << argv.str();
214
215 controller_params.exec->Execve(kCommandFileName,
216 argv_vec,
217 /*envp*/nullptr);
218 // This should never return.
219 }
220 return false;
221 }
222
223 // Gets the perfetto trace infos in the histories.
GetPerfettoTraceInfo(const db::DbHandle & db,const std::vector<db::AppLaunchHistoryModel> & histories)224 std::vector<compiler::CompilationInput> GetPerfettoTraceInfo(
225 const db::DbHandle& db,
226 const std::vector<db::AppLaunchHistoryModel>& histories) {
227 std::vector<compiler::CompilationInput> perfetto_traces;
228
229 for(db::AppLaunchHistoryModel history : histories) {
230 // Get perfetto trace.
231 std::optional<db::RawTraceModel> raw_trace =
232 db::RawTraceModel::SelectByHistoryId(db, history.id);
233 if (!raw_trace) {
234 // This is normal: non-cold launches do not have traces.
235 continue;
236 }
237
238 if (!history.pid) {
239 LOG(DEBUG) << "Missing pid for history " << history.id;
240 continue;
241 }
242
243 uint64_t timestamp_limit = std::numeric_limits<uint64_t>::max();
244 // Get corresponding timestamp limit.
245 if (history.report_fully_drawn_ns) {
246 timestamp_limit = *history.report_fully_drawn_ns;
247 } else if (history.total_time_ns) {
248 timestamp_limit = *history.total_time_ns;
249 } else {
250 LOG(DEBUG) << " No timestamp exists. Using the max value.";
251 }
252 perfetto_traces.push_back({raw_trace->file_path, timestamp_limit, history.pid});
253 }
254 return perfetto_traces;
255 }
256
257 // Helper struct for printing vector.
258 template <class T>
259 struct VectorPrinter {
260 std::vector<T>& values;
261 };
262
operator <<(std::ostream & os,const struct compiler::CompilationInput & perfetto_trace)263 std::ostream& operator<<(std::ostream& os,
264 const struct compiler::CompilationInput& perfetto_trace) {
265 os << "file_path: " << perfetto_trace.filename << " "
266 << "timestamp_limit: " << perfetto_trace.timestamp_limit_ns;
267 return os;
268 }
269
270 template <class T>
operator <<(std::ostream & os,const struct VectorPrinter<T> & printer)271 std::ostream& operator<<(std::ostream& os, const struct VectorPrinter<T>& printer) {
272 os << "[\n";
273 for (T i : printer.values) {
274 os << i << ",\n";
275 }
276 os << "]\n";
277 return os;
278 }
279
280 // Compiled the perfetto traces for an activity.
CompileActivity(const db::DbHandle & db,int package_id,const std::string & package_name,const std::string & activity_name,int version,const ControllerParameters & params)281 bool CompileActivity(const db::DbHandle& db,
282 int package_id,
283 const std::string& package_name,
284 const std::string& activity_name,
285 int version,
286 const ControllerParameters& params) {
287 ScopedFormatTrace atrace_compile_package(ATRACE_TAG_PACKAGE_MANAGER,
288 "Compile activity %s",
289 activity_name.c_str());
290
291 LOG(DEBUG) << "CompileActivity: " << package_name << "/" << activity_name << "@" << version;
292
293 db::CompiledTraceFileModel output_file =
294 CalculateNewestFilePath(package_name, activity_name, version);
295
296 std::string file_path = output_file.FilePath();
297
298 if (!params.recompile) {
299 if (std::filesystem::exists(file_path)) {
300 LOG(DEBUG) << "compiled trace exists in " << file_path;
301
302 db::VersionedComponentName vcn{package_name, activity_name, version};
303 std::optional<db::PrefetchFileModel> prefetch_file =
304 db::PrefetchFileModel::SelectByVersionedComponentName(db, vcn);
305 if (prefetch_file) {
306 return true;
307 } else {
308 LOG(WARNING) << "Missing corresponding prefetch_file db row for " << vcn;
309 // let it go and compile again. we'll insert the prefetch_file at the bottom.
310 }
311 }
312 }
313
314 std::optional<db::ActivityModel> activity =
315 db::ActivityModel::SelectByNameAndPackageId(db, activity_name.c_str(), package_id);
316 if (!activity) {
317 LOG(ERROR) << "Cannot find activity for package_id: " << package_id
318 <<" activity_name: " <<activity_name;
319 return false;
320 }
321
322 int activity_id = activity->id;
323
324 std::vector<db::AppLaunchHistoryModel> histories =
325 db::AppLaunchHistoryModel::SelectActivityHistoryForCompile(db, activity_id);
326
327 {
328 std::vector<compiler::CompilationInput> perfetto_traces =
329 GetPerfettoTraceInfo(db, histories);
330
331 if (perfetto_traces.size() < params.min_traces) {
332 LOG(DEBUG) << "The number of perfetto traces is " << perfetto_traces.size()
333 <<", which is less than " << params.min_traces;
334 return false;
335 }
336
337 {
338 std::lock_guard<std::mutex> last_job_info_guard{last_job_info_mutex_};
339 last_job_info_.activities_last_compiled_++;
340 }
341
342 // Show the compilation config.
343 LOG(DEBUG) << "Try to compiled package_id: " << package_id
344 << " package_name: " << package_name
345 << " activity_name: " << activity_name
346 << " version: " << version
347 << " file_path: " << file_path
348 << " verbose: " << params.verbose
349 << " perfetto_traces: "
350 << VectorPrinter<compiler::CompilationInput>{perfetto_traces};
351 if (params.inode_textcache) {
352 LOG(DEBUG) << "inode_textcache: " << *params.inode_textcache;
353 }
354
355 CompilerForkParameters compiler_params{perfetto_traces, file_path, params};
356
357 if (!output_file.MkdirWithParents()) {
358 LOG(ERROR) << "Compile activity failed. Failed to mkdirs " << file_path;
359 return false;
360 }
361
362 ScopedFormatTrace atrace_compile_fork(ATRACE_TAG_PACKAGE_MANAGER,
363 "Fork+exec iorap.cmd.compiler",
364 activity_name.c_str());
365 if (!StartViaFork(compiler_params)) {
366 LOG(ERROR) << "Compilation failed for package_id:" << package_id
367 << " activity_name: " << activity_name;
368 return false;
369 }
370 }
371
372 std::optional<db::PrefetchFileModel> compiled_trace =
373 db::PrefetchFileModel::Insert(db, activity_id, file_path);
374 if (!compiled_trace) {
375 LOG(ERROR) << "Cannot insert compiled trace activity_id: " << activity_id
376 << " file_path: " << file_path;
377 return false;
378 }
379 return true;
380 }
381
382 // Compiled the perfetto traces for activities in an package.
CompilePackage(const db::DbHandle & db,const std::string & package_name,int version,const ControllerParameters & params)383 bool CompilePackage(const db::DbHandle& db,
384 const std::string& package_name,
385 int version,
386 const ControllerParameters& params) {
387 ScopedFormatTrace atrace_compile_package(ATRACE_TAG_PACKAGE_MANAGER,
388 "Compile package %s",
389 package_name.c_str());
390
391 std::optional<db::PackageModel> package =
392 db::PackageModel::SelectByNameAndVersion(db, package_name.c_str(), version);
393
394 if (!package) {
395 LOG(ERROR) << "Cannot find package for package_name: "
396 << package_name
397 << " and version "
398 << version;
399 return false;
400 }
401
402 std::vector<db::ActivityModel> activities =
403 db::ActivityModel::SelectByPackageId(db, package->id);
404
405 bool ret = true;
406 for (db::ActivityModel activity : activities) {
407 if (!CompileActivity(db, package->id, package->name, activity.name, version, params)) {
408 ret = false;
409 }
410 }
411 return ret;
412 }
413
414 // Compiled the perfetto traces for packages in a device.
CompileAppsOnDevice(const db::DbHandle & db,const ControllerParameters & params)415 bool CompileAppsOnDevice(const db::DbHandle& db, const ControllerParameters& params) {
416 {
417 std::lock_guard<std::mutex> last_job_info_guard{last_job_info_mutex_};
418 last_job_info_.activities_last_compiled_ = 0;
419 }
420
421 std::vector<db::PackageModel> packages = db::PackageModel::SelectAll(db);
422 bool ret = true;
423 for (db::PackageModel package : packages) {
424 if (!CompilePackage(db, package.name, package.version, params)) {
425 ret = false;
426 }
427 }
428
429 {
430 std::lock_guard<std::mutex> last_job_info_guard{last_job_info_mutex_};
431 last_job_info_.last_run_ns_ = time(nullptr);
432 }
433
434 return ret;
435 }
436
437 // Compiled the perfetto traces for a single package in a device.
CompileSingleAppOnDevice(const db::DbHandle & db,const ControllerParameters & params,const std::string & package_name)438 bool CompileSingleAppOnDevice(const db::DbHandle& db,
439 const ControllerParameters& params,
440 const std::string& package_name) {
441 std::vector<db::PackageModel> packages = db::PackageModel::SelectByName(db, package_name.c_str());
442 bool ret = true;
443 for (db::PackageModel package : packages) {
444 if (!CompilePackage(db, package.name, package.version, params)) {
445 ret = false;
446 }
447 }
448
449 return ret;
450 }
451
Compile(const std::string & db_path,const ControllerParameters & params)452 bool Compile(const std::string& db_path, const ControllerParameters& params) {
453 iorap::db::SchemaModel db_schema = db::SchemaModel::GetOrCreate(db_path);
454 db::DbHandle db{db_schema.db()};
455 return CompileAppsOnDevice(db, params);
456 }
457
Compile(const std::string & db_path,const std::string & package_name,int version,const ControllerParameters & params)458 bool Compile(const std::string& db_path,
459 const std::string& package_name,
460 int version,
461 const ControllerParameters& params) {
462 iorap::db::SchemaModel db_schema = db::SchemaModel::GetOrCreate(db_path);
463 db::DbHandle db{db_schema.db()};
464 return CompilePackage(db, package_name, version, params);
465 }
466
Compile(const std::string & db_path,const std::string & package_name,const std::string & activity_name,int version,const ControllerParameters & params)467 bool Compile(const std::string& db_path,
468 const std::string& package_name,
469 const std::string& activity_name,
470 int version,
471 const ControllerParameters& params) {
472 iorap::db::SchemaModel db_schema = db::SchemaModel::GetOrCreate(db_path);
473 db::DbHandle db{db_schema.db()};
474
475 std::optional<db::PackageModel> package =
476 db::PackageModel::SelectByNameAndVersion(db, package_name.c_str(), version);
477
478 if (!package) {
479 LOG(ERROR) << "Cannot find package with name "
480 << package_name
481 << " and version "
482 << version;
483 return false;
484 }
485 return CompileActivity(db, package->id, package_name, activity_name, version, params);
486 }
487
TimeToString(time_t the_time)488 static std::string TimeToString(time_t the_time) {
489 tm tm_buf{};
490 tm* tm_ptr = localtime_r(&the_time, &tm_buf);
491
492 if (tm_ptr != nullptr) {
493 char time_buffer[256];
494 strftime(time_buffer, sizeof(time_buffer), "%a %b %d %H:%M:%S %Y", tm_ptr);
495 return std::string{time_buffer};
496 } else {
497 return std::string{"(nullptr)"};
498 }
499 }
500
GetTimestampForPrefetchFile(const db::PrefetchFileModel & prefetch_file)501 static std::string GetTimestampForPrefetchFile(const db::PrefetchFileModel& prefetch_file) {
502 std::filesystem::path path{prefetch_file.file_path};
503
504 std::error_code ec{};
505 auto last_write_time = std::filesystem::last_write_time(path, /*out*/ec);
506 if (ec) {
507 return std::string("Failed to get last write time: ") + ec.message();
508 }
509
510 time_t time = decltype(last_write_time)::clock::to_time_t(last_write_time);
511
512 std::string time_str = TimeToString(time);
513 return time_str;
514 }
515
DumpPackageActivity(const db::DbHandle & db,::android::Printer & printer,const db::PackageModel & package,const db::ActivityModel & activity)516 void DumpPackageActivity(const db::DbHandle& db,
517 ::android::Printer& printer,
518 const db::PackageModel& package,
519 const db::ActivityModel& activity) {
520 int package_id = package.id;
521 const std::string& package_name = package.name;
522 int package_version = package.version;
523 const std::string& activity_name = activity.name;
524 db::VersionedComponentName vcn{package_name, activity_name, package_version};
525
526 // com.google.Settings/com.google.Settings.ActivityMain@1234567890
527 printer.printFormatLine(" %s/%s@%d",
528 package_name.c_str(),
529 activity_name.c_str(),
530 package_version);
531
532 std::optional<db::PrefetchFileModel> prefetch_file =
533 db::PrefetchFileModel::SelectByVersionedComponentName(db, vcn);
534
535 std::vector<db::AppLaunchHistoryModel> histories =
536 db::AppLaunchHistoryModel::SelectActivityHistoryForCompile(db, activity.id);
537 std::vector<compiler::CompilationInput> perfetto_traces =
538 GetPerfettoTraceInfo(db, histories);
539
540 if (prefetch_file) {
541 bool exists_on_disk = std::filesystem::exists(prefetch_file->file_path);
542
543 std::optional<size_t> prefetch_byte_sum =
544 prefetcher::ReadAhead::PrefetchSizeInBytes(prefetch_file->file_path);
545
546 if (exists_on_disk) {
547 printer.printFormatLine(" Compiled Status: Usable compiled trace");
548 } else {
549 printer.printFormatLine(" Compiled Status: Prefetch file deleted from disk.");
550 }
551
552 if (prefetch_byte_sum) {
553 printer.printFormatLine(" Bytes to be prefetched: %zu", *prefetch_byte_sum);
554 } else {
555 printer.printFormatLine(" Bytes to be prefetched: (bad file path)" );
556 }
557
558 printer.printFormatLine(" Time compiled: %s",
559 GetTimestampForPrefetchFile(*prefetch_file).c_str());
560 printer.printFormatLine(" %s", prefetch_file->file_path.c_str());
561 } else {
562 size_t size = perfetto_traces.size();
563
564 if (size >= kMinTracesForCompilation) {
565 printer.printFormatLine(" Compiled Status: Raw traces pending compilation (%zu)",
566 perfetto_traces.size());
567 } else {
568 size_t remaining = kMinTracesForCompilation - size;
569 printer.printFormatLine(" Compiled Status: Need %zu more traces for compilation",
570 remaining);
571 }
572 }
573
574 printer.printFormatLine(" Raw traces:");
575 printer.printFormatLine(" Trace count: %zu", perfetto_traces.size());
576
577 for (compiler::CompilationInput& compilation_input : perfetto_traces) {
578 std::string& raw_trace_file_name = compilation_input.filename;
579
580 printer.printFormatLine(" %s", raw_trace_file_name.c_str());
581 }
582 }
583
DumpPackage(const db::DbHandle & db,::android::Printer & printer,db::PackageModel package)584 void DumpPackage(const db::DbHandle& db,
585 ::android::Printer& printer,
586 db::PackageModel package) {
587 std::vector<db::ActivityModel> activities =
588 db::ActivityModel::SelectByPackageId(db, package.id);
589
590 for (db::ActivityModel& activity : activities) {
591 DumpPackageActivity(db, printer, package, activity);
592 }
593 }
594
DumpAllPackages(const db::DbHandle & db,::android::Printer & printer)595 void DumpAllPackages(const db::DbHandle& db, ::android::Printer& printer) {
596 printer.printLine("Package history in database:");
597
598 std::vector<db::PackageModel> packages = db::PackageModel::SelectAll(db);
599 for (db::PackageModel package : packages) {
600 DumpPackage(db, printer, package);
601 }
602
603 printer.printLine("");
604 }
605
Dump(const db::DbHandle & db,::android::Printer & printer)606 void Dump(const db::DbHandle& db, ::android::Printer& printer) {
607 bool locked = last_job_info_mutex_.try_lock();
608
609 LastJobInfo info = last_job_info_;
610
611 printer.printFormatLine("Background job:");
612 if (!locked) {
613 printer.printLine(""""" (possible deadlock)");
614 }
615 if (info.last_run_ns_ != time_t{0}) {
616 std::string time_str = TimeToString(info.last_run_ns_);
617
618 printer.printFormatLine(" Last run at: %s", time_str.c_str());
619 } else {
620 printer.printFormatLine(" Last run at: (None)");
621 }
622 printer.printFormatLine(" Activities last compiled: %zu", info.activities_last_compiled_);
623
624 printer.printLine("");
625
626 if (locked) {
627 last_job_info_mutex_.unlock();
628 }
629
630 DumpAllPackages(db, printer);
631 }
632
633 } // namespace iorap::maintenance
634