Revert^2 "odrefresh: compilation backoff"

Adds 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.

Relands commit 6859ffca5ffd15128459293046590488008221ff. The
odsign_e2e tests required updating to remove the compilation log whose
purpose is to backoff compilation attempts in the wild.

Bug: 187494247
Test: atest art_odrefresh_tests
Test: atest odsign_e2e_tests
Change-Id: Id41ee875cf1ca376f8e2ae05a43d0f6f74a9995f
diff --git a/odrefresh/Android.bp b/odrefresh/Android.bp
index b5a5eb3..e42539d 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",
@@ -161,6 +162,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 e0720ca..85380a4 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"
@@ -1471,10 +1472,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") {
diff --git a/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java b/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
index 1ac89fd..a8374d1 100644
--- a/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
+++ b/test/odsign/test-src/com/android/tests/odsign/OnDeviceSigningHostTest.java
@@ -43,9 +43,17 @@
 public class OnDeviceSigningHostTest extends BaseHostJUnit4Test {
 
     private static final String APEX_FILENAME = "test_com.android.art.apex";
+
     private static final String ART_APEX_DALVIK_CACHE_DIRNAME =
             "/data/misc/apexdata/com.android.art/dalvik-cache";
 
+    private static final String ODREFRESH_COMPILATION_LOG =
+            "/data/misc/odrefresh/compilation-log.txt";
+
+    private final String[] APP_ARTIFACT_EXTENSIONS = new String[] {".art", ".odex", ".vdex"};
+
+    private final String[] BCP_ARTIFACT_EXTENSIONS = new String[] {".art", ".oat", ".vdex"};
+
     private static final String TEST_APP_PACKAGE_NAME = "com.android.tests.odsign";
     private static final String TEST_APP_APK = "odsign_e2e_test_app.apk";
 
@@ -58,6 +66,7 @@
         assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
         installPackage(TEST_APP_APK);
         mInstallUtils.installApexes(APEX_FILENAME);
+        removeCompilationLogToAvoidBackoff();
         reboot();
     }
 
@@ -65,6 +74,7 @@
     public void cleanup() throws Exception {
         ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(APEX_FILENAME));
         getDevice().uninstallPackage(apex.name);
+        removeCompilationLogToAvoidBackoff();
         reboot();
     }
 
@@ -132,9 +142,6 @@
         final String isa = getSystemServerIsa(mappedArtifacts.iterator().next());
         final String isaCacheDirectory = String.format("%s/%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa);
 
-        // Extension types for artifacts that this test looks for.
-        final String[] extensions = new String[] {".art", ".odex", ".vdex"};
-
         // Check the non-APEX components in the system_server classpath have mapped artifacts.
         for (String element : classpathElements) {
             // Skip system_server classpath elements from APEXes as these are not currently
@@ -143,7 +150,7 @@
                 continue;
             }
             String escapedPath = element.substring(1).replace('/', '@');
-            for (String extension : extensions) {
+            for (String extension : APP_ARTIFACT_EXTENSIONS) {
                 final String fullArtifactPath =
                         String.format("%s/%s@classes%s", isaCacheDirectory, escapedPath, extension);
                 assertTrue(
@@ -162,7 +169,8 @@
 
             // Check the mapped artifact has a .art, .odex or .vdex extension.
             final boolean knownArtifactKind =
-                    Arrays.stream(extensions).anyMatch(e -> mappedArtifact.endsWith(e));
+                    Arrays.stream(APP_ARTIFACT_EXTENSIONS)
+                            .anyMatch(e -> mappedArtifact.endsWith(e));
             assertTrue("Unknown artifact kind: " + mappedArtifact, knownArtifactKind);
         }
     }
@@ -173,10 +181,7 @@
 
         assertTrue("Expect 3 boot-framework artifacts", mappedArtifacts.size() == 3);
 
-        // Extension types for artifacts that this test looks for.
-        final String[] extensions = new String[] {".art", ".oat", ".vdex"};
-
-        for (String extension : extensions) {
+        for (String extension : BCP_ARTIFACT_EXTENSIONS) {
             final String artifact = bootExtensionName + extension;
             final boolean found = mappedArtifacts.stream().anyMatch(a -> a.endsWith(artifact));
             assertTrue(artifact + " not found", found);
@@ -207,6 +212,9 @@
         final boolean adbEnabled = getDevice().enableAdbRoot();
         assertTrue("ADB root failed and required to get process maps", adbEnabled);
 
+        // Check there is a compilation log, we expect compilation to have occurred.
+        assertTrue("Compilation log not found", haveCompilationLog());
+
         // Check both zygote and system_server processes to see that they have loaded the
         // artifacts compiled and signed by odrefresh and odsign. We check both here rather than
         // having a separate test because the device reboots between each @Test method and
@@ -215,6 +223,16 @@
         verifySystemServerLoadedArtifacts();
     }
 
+    private boolean haveCompilationLog() throws Exception {
+        CommandResult result =
+                getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG);
+        return result.getExitCode() == 0;
+    }
+
+    private void removeCompilationLogToAvoidBackoff() throws Exception {
+        getDevice().executeShellCommand("rm -f " + ODREFRESH_COMPILATION_LOG);
+    }
+
     private void reboot() throws Exception {
         getDevice().reboot();
         boolean success = getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());