odrefresh: compilation backoff

Add backoff logic to limit attempts odrefresh tries to compile. It
will always recompile if the APEX is updated or the input JARs change,
but if compilation fails for any reason then odrefresh backs off
exponentially in days.

Bug: 187494247
Test: atest art_odrefresh_tests
Test: manual
Change-Id: I11fe5549038680d03df663135ed087161428f392
diff --git a/odrefresh/Android.bp b/odrefresh/Android.bp
index 10845c5..255acc9 100644
--- a/odrefresh/Android.bp
+++ b/odrefresh/Android.bp
@@ -29,6 +29,7 @@
     defaults: ["art_defaults"],
     srcs: [
         "odrefresh.cc",
+        "odr_compilation_log.cc",
         "odr_fs_utils.cc",
         "odr_metrics.cc",
         "odr_metrics_record.cc",
@@ -168,6 +169,8 @@
     header_libs: ["odrefresh_headers"],
     srcs: [
         "odr_artifacts_test.cc",
+        "odr_compilation_log.cc",
+        "odr_compilation_log_test.cc",
         "odr_fs_utils.cc",
         "odr_fs_utils_test.cc",
         "odr_metrics.cc",
diff --git a/odrefresh/TODO.md b/odrefresh/TODO.md
index 5676398..9d7c9fc 100644
--- a/odrefresh/TODO.md
+++ b/odrefresh/TODO.md
@@ -2,7 +2,7 @@
 
 ## TODO (STOPSHIP until done)
 
-1. Implement back off on trying compilation when previous attempt(s) failed.
+1. denylist for AOT artifacts.
 
 ## DONE
 
@@ -21,5 +21,6 @@
    - Unexpected error (a setup or clean-up action failed).
 6. Metrics recording for subprocess timeouts.
 7. Free space calculation and only attempting compilation if sufficient space.
+8. Implement back off on trying compilation when previous attempt(s) failed.
 
-</strike>
\ No newline at end of file
+</strike>
diff --git a/odrefresh/odr_compilation_log.cc b/odrefresh/odr_compilation_log.cc
new file mode 100644
index 0000000..55432f4
--- /dev/null
+++ b/odrefresh/odr_compilation_log.cc
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <odr_compilation_log.h>
+
+#include <errno.h>
+
+#include <fstream>
+#include <ios>
+#include <iosfwd>
+#include <istream>
+#include <ostream>
+#include <streambuf>
+#include <string>
+#include <vector>
+
+#include "android-base/logging.h"
+#include "base/os.h"
+
+#include "odrefresh/odrefresh.h"
+#include "odr_metrics.h"
+
+namespace art {
+namespace odrefresh {
+
+std::istream& operator>>(std::istream& is, OdrCompilationLogEntry& entry) {
+  // Block I/O related exceptions
+  auto saved_exceptions = is.exceptions();
+  is.exceptions(std::ios_base::iostate {});
+
+  is >> entry.apex_version >> std::ws;
+  is >> entry.trigger >> std::ws;
+  is >> entry.when >> std::ws;
+  is >> entry.exit_code >> std::ws;
+
+  // Restore I/O related exceptions
+  is.exceptions(saved_exceptions);
+  return is;
+}
+
+std::ostream& operator<<(std::ostream& os, const OdrCompilationLogEntry& entry) {
+  static const char kSpace = ' ';
+
+  // Block I/O related exceptions
+  auto saved_exceptions = os.exceptions();
+  os.exceptions(std::ios_base::iostate {});
+
+  os << entry.apex_version << kSpace;
+  os << entry.trigger << kSpace;
+  os << entry.when << kSpace;
+  os << entry.exit_code << std::endl;
+
+  // Restore I/O related exceptions
+  os.exceptions(saved_exceptions);
+  return os;
+}
+
+bool operator==(const OdrCompilationLogEntry& lhs, const OdrCompilationLogEntry& rhs) {
+  return lhs.apex_version == rhs.apex_version && lhs.trigger == rhs.trigger &&
+         lhs.when == rhs.when && lhs.exit_code == rhs.exit_code;
+}
+
+bool operator!=(const OdrCompilationLogEntry& lhs, const OdrCompilationLogEntry& rhs) {
+  return !(lhs == rhs);
+}
+
+OdrCompilationLog::OdrCompilationLog(const char* compilation_log_path)
+    : log_path_(compilation_log_path) {
+  if (log_path_ != nullptr && OS::FileExists(log_path_)) {
+    if (!Read()) {
+      PLOG(ERROR) << "Failed to read compilation log: " << log_path_;
+    }
+  }
+}
+
+OdrCompilationLog::~OdrCompilationLog() {
+  if (log_path_ != nullptr && !Write()) {
+    PLOG(ERROR) << "Failed to write compilation log: " << log_path_;
+  }
+}
+
+bool OdrCompilationLog::Read() {
+  std::ifstream ifs(log_path_);
+  if (!ifs.good()) {
+    return false;
+  }
+
+  while (!ifs.eof()) {
+    OdrCompilationLogEntry entry;
+    ifs >> entry;
+    if (ifs.fail()) {
+      entries_.clear();
+      return false;
+    }
+    entries_.push_back(entry);
+  }
+
+  return true;
+}
+
+bool OdrCompilationLog::Write() const {
+  std::ofstream ofs(log_path_, std::ofstream::trunc);
+  if (!ofs.good()) {
+    return false;
+  }
+
+  for (const auto& entry : entries_) {
+    ofs << entry;
+    if (ofs.fail()) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void OdrCompilationLog::Truncate() {
+  if (entries_.size() < kMaxLoggedEntries) {
+    return;
+  }
+
+  size_t excess = entries_.size() - kMaxLoggedEntries;
+  entries_.erase(entries_.begin(), entries_.begin() + excess);
+}
+
+size_t OdrCompilationLog::NumberOfEntries() const {
+  return entries_.size();
+}
+
+const OdrCompilationLogEntry* OdrCompilationLog::Peek(size_t index) const {
+  if (index >= entries_.size()) {
+    return nullptr;
+  }
+  return &entries_[index];
+}
+
+void OdrCompilationLog::Log(int64_t apex_version,
+                            OdrMetrics::Trigger trigger,
+                            ExitCode compilation_result) {
+  time_t now;
+  time(&now);
+  Log(apex_version, trigger, now, compilation_result);
+}
+
+void OdrCompilationLog::Log(int64_t apex_version,
+                            OdrMetrics::Trigger trigger,
+                            time_t when,
+                            ExitCode compilation_result) {
+  entries_.push_back(OdrCompilationLogEntry{
+      apex_version, static_cast<int32_t>(trigger), when, static_cast<int32_t>(compilation_result)});
+  Truncate();
+}
+
+bool OdrCompilationLog::ShouldAttemptCompile(int64_t apex_version,
+                                             OdrMetrics::Trigger trigger,
+                                             time_t now) const {
+  if (entries_.size() == 0) {
+    // We have no history, try to compile.
+    return true;
+  }
+
+  if (apex_version != entries_.back().apex_version) {
+    // There is a new ART APEX, we should use compile right away.
+    return true;
+  }
+
+  if (trigger == OdrMetrics::Trigger::kDexFilesChanged) {
+    // The DEX files in the classpaths have changed, possibly an OTA has updated them.
+    return true;
+  }
+
+  // Compute the backoff time based on the number of consecutive failures.
+  //
+  // Wait 12 hrs * pow(2, consecutive_failures) since the last compilation attempt.
+  static const int kSecondsPerDay = 86'400;
+  time_t backoff = kSecondsPerDay / 2;
+  for (auto it = entries_.crbegin(); it != entries_.crend(); ++it, backoff *= 2) {
+    if (it->exit_code == ExitCode::kCompilationSuccess) {
+      break;
+    }
+  }
+
+  if (now == 0) {
+    time(&now);
+  }
+
+  const time_t last_attempt = entries_.back().when;
+  const time_t threshold = last_attempt + backoff;
+  return now >= threshold;
+}
+
+}  // namespace odrefresh
+}  // namespace art
diff --git a/odrefresh/odr_compilation_log.h b/odrefresh/odr_compilation_log.h
new file mode 100644
index 0000000..6f13c97
--- /dev/null
+++ b/odrefresh/odr_compilation_log.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef ART_ODREFRESH_ODR_COMPILATION_LOG_H_
+#define ART_ODREFRESH_ODR_COMPILATION_LOG_H_
+
+#include <time.h>
+
+#include <cstdint>
+#include <iosfwd>
+#include <vector>
+
+#include <odrefresh/odrefresh.h>
+#include <odr_metrics.h>
+
+namespace art {
+namespace odrefresh {
+
+// OdrCompilationLogEntry represents the result of a compilation attempt by odrefresh.
+struct OdrCompilationLogEntry {
+  int64_t apex_version;
+  int32_t trigger;
+  time_t when;
+  int32_t exit_code;
+};
+
+// Read an `OdrCompilationLogEntry` from an input stream.
+std::istream& operator>>(std::istream& is, OdrCompilationLogEntry& entry);
+
+// Write an `OdrCompilationLogEntry` to an output stream.
+std::ostream& operator<<(std::ostream& os, const OdrCompilationLogEntry& entry);
+
+// Equality test for two `OdrCompilationLogEntry` instances.
+bool operator==(const OdrCompilationLogEntry& lhs, const OdrCompilationLogEntry& rhs);
+bool operator!=(const OdrCompilationLogEntry& lhs, const OdrCompilationLogEntry& rhs);
+
+class OdrCompilationLog {
+ public:
+  // The compilation log location is in the same directory as used for the metricss.log. This
+  // directory is only used by odrefresh whereas the ART apexdata directory is also used by odsign
+  // and others which may lead to the deletion (or rollback) of the log file.
+  static constexpr const char* kCompilationLogFile = "/data/misc/odrefresh/compilation-log.txt";
+  static constexpr const size_t kMaxLoggedEntries = 4;
+
+  explicit OdrCompilationLog(const char* compilation_log_path = kCompilationLogFile);
+  ~OdrCompilationLog();
+
+  // Applies policy to compilation log to determine whether to recompile.
+  bool ShouldAttemptCompile(int64_t apex_version,
+                            OdrMetrics::Trigger trigger,
+                            time_t now = 0) const;
+
+  // Returns the number of entries in the log. The log never exceeds `kMaxLoggedEntries`.
+  size_t NumberOfEntries() const;
+
+  // Returns the entry at position `index` or nullptr if `index` is out of bounds.
+  const OdrCompilationLogEntry* Peek(size_t index) const;
+
+  void Log(int64_t apex_version, OdrMetrics::Trigger trigger, ExitCode compilation_result);
+
+  void Log(int64_t apex_version,
+           OdrMetrics::Trigger trigger,
+           time_t when,
+           ExitCode compilation_result);
+
+  // Truncates the in memory log to have `kMaxLoggedEntries` records.
+  void Truncate();
+
+ private:
+  bool Read();
+  bool Write() const;
+
+  std::vector<OdrCompilationLogEntry> entries_;
+  const char* log_path_;
+};
+
+}  // namespace odrefresh
+}  // namespace art
+
+#endif  // ART_ODREFRESH_ODR_COMPILATION_LOG_H_
diff --git a/odrefresh/odr_compilation_log_test.cc b/odrefresh/odr_compilation_log_test.cc
new file mode 100644
index 0000000..c5c9555
--- /dev/null
+++ b/odrefresh/odr_compilation_log_test.cc
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <odr_compilation_log.h>
+
+#include <time.h>
+
+#include <cstdint>
+#include <ctime>
+#include <iosfwd>
+#include <istream>
+#include <limits>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "base/common_art_test.h"
+
+#include "odrefresh/odrefresh.h"
+#include "odr_metrics.h"
+
+namespace art {
+namespace odrefresh {
+
+const time_t kSecondsPerDay = 86'400;
+
+class OdrCompilationLogTest : public CommonArtTest {};
+
+TEST(OdrCompilationLogEntry, Equality) {
+  OdrCompilationLogEntry a{1, 2, 3, 4};
+
+  ASSERT_EQ(a, (OdrCompilationLogEntry{1, 2, 3, 4}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{9, 2, 3, 4}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{1, 9, 3, 4}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{1, 2, 9, 4}));
+  ASSERT_NE(a, (OdrCompilationLogEntry{2, 2, 3, 9}));
+}
+
+TEST(OdrCompilationLogEntry, InputOutput) {
+  const OdrCompilationLogEntry entries[] = {
+      {1, 2, 3, 4},
+      {std::numeric_limits<int64_t>::min(),
+       std::numeric_limits<int32_t>::min(),
+       std::numeric_limits<time_t>::min(),
+       std::numeric_limits<int32_t>::min()},
+      {std::numeric_limits<int64_t>::max(),
+       std::numeric_limits<int32_t>::max(),
+       std::numeric_limits<time_t>::max(),
+       std::numeric_limits<int32_t>::max()},
+       {0, 0, 0, 0},
+      {0x7fedcba9'87654321, 0x12345678, 0x2346789, 0x76543210}
+  };
+  for (const auto& entry : entries) {
+    std::stringstream ss;
+    ss << entry;
+    OdrCompilationLogEntry actual;
+    ss >> actual;
+    ASSERT_EQ(entry, actual);
+  }
+}
+
+TEST(OdrCompilationLogEntry, TruncatedInput) {
+  std::stringstream ss;
+  ss << "1 2";
+
+  OdrCompilationLogEntry entry;
+  ss >> entry;
+
+  ASSERT_TRUE(ss.fail());
+  ASSERT_FALSE(ss.bad());
+}
+
+TEST(OdrCompilationLogEntry, ReadMultiple) {
+  std::stringstream ss;
+  ss << "1 2 3 4\n5 6 7 8\n";
+
+  OdrCompilationLogEntry entry0, entry1;
+  ss >> entry0 >> entry1;
+  ASSERT_EQ(entry0, (OdrCompilationLogEntry{1, 2, 3, 4}));
+  ASSERT_EQ(entry1, (OdrCompilationLogEntry{5, 6, 7, 8}));
+
+  ASSERT_FALSE(ss.fail());
+  ASSERT_FALSE(ss.bad());
+}
+
+TEST(OdrCompilationLog, ShouldAttemptCompile) {
+  OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
+
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kMissingArtifacts, 0));
+
+  ocl.Log(
+      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, ExitCode::kCompilationSuccess);
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(2, OdrMetrics::Trigger::kApexVersionMismatch));
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kApexVersionMismatch));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kDexFilesChanged));
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(1, OdrMetrics::Trigger::kUnknown));
+}
+
+TEST(OdrCompilationLog, BackOffNoHistory) {
+  time_t start_time;
+  time(&start_time);
+
+  OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
+
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+
+  // Start log
+  ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationFailed);
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay / 2));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay));
+
+  // Add one more log entry
+  ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationFailed);
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + 2 * kSecondsPerDay));
+
+  // One more.
+  ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationFailed);
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + 3 * kSecondsPerDay));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + 4 * kSecondsPerDay));
+
+  // And one for the road.
+  ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationFailed);
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + 7 * kSecondsPerDay));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + 8 * kSecondsPerDay));
+}
+
+TEST(OdrCompilationLog, BackOffHappyHistory) {
+  time_t start_time;
+  time(&start_time);
+
+  OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
+
+  // Start log with a successful entry.
+  ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationSuccess);
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay / 4));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay / 2));
+
+    // Add a log entry for a failed compilation.
+  ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationFailed);
+  ASSERT_FALSE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay / 2));
+  ASSERT_TRUE(ocl.ShouldAttemptCompile(
+      /*apex_version=*/1,
+      OdrMetrics::Trigger::kApexVersionMismatch,
+      start_time + kSecondsPerDay));
+}
+
+TEST_F(OdrCompilationLogTest, LogNumberOfEntriesAndPeek) {
+  OdrCompilationLog ocl(/*compilation_log_path=*/nullptr);
+
+  std::vector<OdrCompilationLogEntry> entries = {
+    { 0, 1, 2, 3 },
+    { 1, 2, 3, 4 },
+    { 2, 3, 4, 5 },
+    { 3, 4, 5, 6 },
+    { 4, 5, 6, 7 },
+    { 5, 6, 7, 8 },
+    { 6, 7, 8, 9 }
+  };
+
+  for (size_t i = 0; i < entries.size(); ++i) {
+    OdrCompilationLogEntry& e = entries[i];
+    ocl.Log(e.apex_version,
+            static_cast<OdrMetrics::Trigger>(e.trigger),
+            e.when,
+            static_cast<ExitCode>(e.exit_code));
+    if (i < OdrCompilationLog::kMaxLoggedEntries) {
+      ASSERT_EQ(i + 1, ocl.NumberOfEntries());
+    } else {
+      ASSERT_EQ(OdrCompilationLog::kMaxLoggedEntries, ocl.NumberOfEntries());
+    }
+
+    for (size_t j = 0; j < ocl.NumberOfEntries(); ++j) {
+      const OdrCompilationLogEntry* logged = ocl.Peek(j);
+      ASSERT_TRUE(logged != nullptr);
+      const OdrCompilationLogEntry& expected = entries[i + 1 - ocl.NumberOfEntries() + j];
+      ASSERT_EQ(expected, *logged);
+    }
+  }
+}
+
+TEST_F(OdrCompilationLogTest, LogReadWrite) {
+  std::vector<OdrCompilationLogEntry> entries = {
+    { 0, 1, 2, 3 },
+    { 1, 2, 3, 4 },
+    { 2, 3, 4, 5 },
+    { 3, 4, 5, 6 },
+    { 4, 5, 6, 7 },
+    { 5, 6, 7, 8 },
+    { 6, 7, 8, 9 }
+  };
+
+  ScratchFile scratch_file;
+  scratch_file.Close();
+
+  for (size_t i = 0; i < entries.size(); ++i) {
+    {
+      OdrCompilationLog ocl(scratch_file.GetFilename().c_str());
+      OdrCompilationLogEntry& e = entries[i];
+      ocl.Log(e.apex_version,
+              static_cast<OdrMetrics::Trigger>(e.trigger),
+              e.when,
+              static_cast<ExitCode>(e.exit_code));
+    }
+
+    {
+      OdrCompilationLog ocl(scratch_file.GetFilename().c_str());
+      if (i < OdrCompilationLog::kMaxLoggedEntries) {
+        ASSERT_EQ(i + 1, ocl.NumberOfEntries());
+      } else {
+        ASSERT_EQ(OdrCompilationLog::kMaxLoggedEntries, ocl.NumberOfEntries());
+      }
+
+      for (size_t j = 0; j < ocl.NumberOfEntries(); ++j) {
+        const OdrCompilationLogEntry* logged = ocl.Peek(j);
+        ASSERT_TRUE(logged != nullptr);
+        const OdrCompilationLogEntry& expected = entries[i + 1 - ocl.NumberOfEntries() + j];
+        ASSERT_EQ(expected, *logged);
+      }
+    }
+  }
+}
+
+TEST_F(OdrCompilationLogTest, BackoffBasedOnLog) {
+  time_t start_time;
+  time(&start_time);
+
+  ScratchFile scratch_file;
+  scratch_file.Close();
+
+  const char* log_path = scratch_file.GetFilename().c_str();
+  {
+    OdrCompilationLog ocl(log_path);
+
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+  }
+
+  {
+    OdrCompilationLog ocl(log_path);
+
+    // Start log
+    ocl.Log(/*apex_version=*/1,
+            OdrMetrics::Trigger::kApexVersionMismatch,
+            start_time,
+            ExitCode::kCompilationFailed);
+  }
+
+  {
+    OdrCompilationLog ocl(log_path);
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1, OdrMetrics::Trigger::kApexVersionMismatch, start_time));
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + kSecondsPerDay / 2));
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + kSecondsPerDay));
+  }
+
+  {
+    // Add one more log entry
+    OdrCompilationLog ocl(log_path);
+    ocl.Log(/*apex_version=*/1,
+            OdrMetrics::Trigger::kApexVersionMismatch,
+            start_time,
+            ExitCode::kCompilationFailed);
+  }
+
+  {
+    OdrCompilationLog ocl(log_path);
+
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + kSecondsPerDay));
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 2 * kSecondsPerDay));
+  }
+
+  {
+    // One more log entry.
+    OdrCompilationLog ocl(log_path);
+    ocl.Log(/*apex_version=*/1,
+          OdrMetrics::Trigger::kApexVersionMismatch,
+          start_time,
+          ExitCode::kCompilationFailed);
+  }
+
+  {
+    OdrCompilationLog ocl(log_path);
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 3 * kSecondsPerDay));
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 4 * kSecondsPerDay));
+  }
+
+  {
+    // And one for the road.
+    OdrCompilationLog ocl(log_path);
+    ocl.Log(/*apex_version=*/1,
+            OdrMetrics::Trigger::kApexVersionMismatch,
+            start_time,
+            ExitCode::kCompilationFailed);
+  }
+
+  {
+    OdrCompilationLog ocl(log_path);
+    ASSERT_FALSE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 7 * kSecondsPerDay));
+    ASSERT_TRUE(ocl.ShouldAttemptCompile(
+        /*apex_version=*/1,
+        OdrMetrics::Trigger::kApexVersionMismatch,
+        start_time + 8 * kSecondsPerDay));
+  }
+}
+
+}  // namespace odrefresh
+}  // namespace art
diff --git a/odrefresh/odr_metrics.h b/odrefresh/odr_metrics.h
index 8b8d5ff..5ff9df2 100644
--- a/odrefresh/odr_metrics.h
+++ b/odrefresh/odr_metrics.h
@@ -74,11 +74,22 @@
                       const std::string& metrics_file = kOdrefreshMetricsFile);
   ~OdrMetrics();
 
+  // Gets the ART APEX that metrics are being collected on behalf of.
+  int64_t GetApexVersion() const {
+    return art_apex_version_;
+  }
+
   // Sets the ART APEX that metrics are being collected on behalf of.
   void SetArtApexVersion(int64_t version) {
     art_apex_version_ = version;
   }
 
+  // Gets the trigger for metrics collection. The trigger is the reason why odrefresh considers
+  // compilation necessary.
+  Trigger GetTrigger() const {
+    return trigger_.has_value() ? trigger_.value() : Trigger::kUnknown;
+  }
+
   // Sets the trigger for metrics collection. The trigger is the reason why odrefresh considers
   // compilation necessary. Only call this method if compilation is necessary as the presence
   // of a trigger means we will try to record and upload metrics.
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index d744dfc..66e2758 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -69,6 +69,7 @@
 #include "palette/palette_types.h"
 
 #include "odr_artifacts.h"
+#include "odr_compilation_log.h"
 #include "odr_config.h"
 #include "odr_fs_utils.h"
 #include "odr_metrics.h"
@@ -1456,10 +1457,16 @@
         return odr.CheckArtifactsAreUpToDate(metrics);
       } else if (action == "--compile") {
         const ExitCode exit_code = odr.CheckArtifactsAreUpToDate(metrics);
-        if (exit_code == ExitCode::kCompilationRequired) {
-          return odr.Compile(metrics, /*force_compile=*/false);
+        if (exit_code != ExitCode::kCompilationRequired) {
+          return exit_code;
         }
-        return exit_code;
+        OdrCompilationLog compilation_log;
+        if (!compilation_log.ShouldAttemptCompile(metrics.GetApexVersion(), metrics.GetTrigger())) {
+          return ExitCode::kOkay;
+        }
+        ExitCode compile_result = odr.Compile(metrics, /*force_compile=*/false);
+        compilation_log.Log(metrics.GetApexVersion(), metrics.GetTrigger(), compile_result);
+        return compile_result;
       } else if (action == "--force-compile") {
         return odr.Compile(metrics, /*force_compile=*/true);
       } else if (action == "--verify") {