Revert^2 "odrefresh: compilation backoff" am: f761f58879

Original change: https://android-review.googlesource.com/c/platform/art/+/1729804

Change-Id: I663fd5a297d06b65d379d41eceb2a790d1f6b480
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());