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