Merge "Fix performance bug in logging: Avoid expensive stack trace/reflection"
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index 61d609b..9501e52 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -218,7 +218,7 @@
 
     logger.atFine().log(
         "Executing %d %s index queries for %s",
-        cnt, schemaDef.getName(), callerFinder.findCaller());
+        cnt, schemaDef.getName(), callerFinder.findCallerLazy());
     List<QueryResult<T>> out;
     try {
       // Parse and rewrite all queries.
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 2c72f56..d75aa17 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -675,7 +675,8 @@
   @Override
   protected void onLoad() throws IOException, ConfigInvalidException {
     if (revision != null) {
-      logger.atFine().log("Reading external ID note map (caller: %s)", callerFinder.findCaller());
+      logger.atFine().log(
+          "Reading external ID note map (caller: %s)", callerFinder.findCallerLazy());
       noteMap = NoteMap.read(reader, revision);
     } else {
       noteMap = NoteMap.newEmptyMap();
diff --git a/java/com/google/gerrit/server/logging/CallerFinder.java b/java/com/google/gerrit/server/logging/CallerFinder.java
index 73ffeb5..bd7e608 100644
--- a/java/com/google/gerrit/server/logging/CallerFinder.java
+++ b/java/com/google/gerrit/server/logging/CallerFinder.java
@@ -142,7 +142,7 @@
 
   /**
    * The minimum number of calls known to have occurred between the first call to the target class
-   * and the call of {@link #findCaller()}. If in doubt, specify zero here to avoid accidentally
+   * and the call of {@link #findCallerLazy()}. If in doubt, specify zero here to avoid accidentally
    * skipping past the caller.
    *
    * @return the number of stack elements to skip when computing the caller
@@ -195,7 +195,7 @@
     public abstract CallerFinder build();
   }
 
-  public LazyArg<String> findCaller() {
+  public LazyArg<String> findCallerLazy() {
     return lazy(
         () ->
             targets().stream()
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index dae78c0..c3df289 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -18,11 +18,14 @@
 import com.google.common.base.MoreObjects;
 import com.google.common.base.MoreObjects.ToStringHelper;
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.LazyArg;
+import com.google.common.flogger.LazyArgs;
 import com.google.gerrit.common.Nullable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Optional;
 
 /** Metadata that is provided to {@link PerformanceLogger}s as context for performance records. */
@@ -141,7 +144,8 @@
   public abstract Optional<String> username();
 
   /**
-   * Returns a string representation of this instance that is suitable for logging.
+   * Returns a string representation of this instance that is suitable for logging. This is wrapped
+   * in a {@link LazyArg<String>} because it is expensive to evaluate.
    *
    * <p>{@link #toString()} formats the {@link Optional} fields as {@code key=Optional[value]} or
    * {@code key=Optional.empty}. Since this class has many optional fields from which usually only a
@@ -178,59 +182,64 @@
    *
    * @return string representation of this instance that is suitable for logging
    */
-  public String toStringForLogging() {
-    // Append class name.
-    String className = getClass().getSimpleName();
-    if (className.startsWith("AutoValue_")) {
-      className = className.substring(10);
-    }
-    ToStringHelper stringHelper = MoreObjects.toStringHelper(className);
+  LazyArg<String> toStringForLoggingLazy() {
+    return LazyArgs.lazy(
+        () -> {
+          // Append class name.
+          String className = getClass().getSimpleName();
+          if (className.startsWith("AutoValue_")) {
+            className = className.substring(10);
+          }
+          ToStringHelper stringHelper = MoreObjects.toStringHelper(className);
 
-    // Append key-value pairs for field which are set.
-    Method[] methods = Metadata.class.getDeclaredMethods();
-    Arrays.<Method>sort(methods, (m1, m2) -> m1.getName().compareTo(m2.getName()));
-    for (Method method : methods) {
-      if (Modifier.isStatic(method.getModifiers())) {
-        // skip static method
-        continue;
-      }
+          // Append key-value pairs for field which are set.
+          Method[] methods = Metadata.class.getDeclaredMethods();
+          Arrays.sort(methods, Comparator.comparing(Method::getName));
+          for (Method method : methods) {
+            if (Modifier.isStatic(method.getModifiers())) {
+              // skip static method
+              continue;
+            }
 
-      if (method.getName().equals(Thread.currentThread().getStackTrace()[1].getMethodName())) {
-        // skip this method (toStringForLogging() method)
-        continue;
-      }
+            if (method.getName().matches("(lambda\\$)?toStringForLoggingLazy(\\$0)?")) {
+              // skip toStringForLoggingLazy() and the lambda itself
+              continue;
+            }
 
-      if (method.getReturnType().equals(Void.TYPE) || method.getParameterCount() > 0) {
-        // skip method since it's not a getter
-        continue;
-      }
+            if (method.getReturnType().equals(Void.TYPE) || method.getParameterCount() > 0) {
+              // skip method since it's not a getter
+              continue;
+            }
 
-      method.setAccessible(true);
+            method.setAccessible(true);
 
-      Object returnValue;
-      try {
-        returnValue = method.invoke(this);
-      } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
-        // should never happen
-        throw new IllegalStateException(e);
-      }
+            Object returnValue;
+            try {
+              returnValue = method.invoke(this);
+            } catch (IllegalArgumentException
+                | IllegalAccessException
+                | InvocationTargetException e) {
+              // should never happen
+              throw new IllegalStateException(e);
+            }
 
-      if (returnValue instanceof Optional) {
-        Optional<?> fieldValueOptional = (Optional<?>) returnValue;
-        if (!fieldValueOptional.isPresent()) {
-          // drop this key-value pair
-          continue;
-        }
+            if (returnValue instanceof Optional) {
+              Optional<?> fieldValueOptional = (Optional<?>) returnValue;
+              if (!fieldValueOptional.isPresent()) {
+                // drop this key-value pair
+                continue;
+              }
 
-        // format as 'key=value' instead of 'key=Optional[value]'
-        stringHelper.add(method.getName(), fieldValueOptional.get());
-      } else {
-        // not an Optional value, keep as is
-        stringHelper.add(method.getName(), returnValue);
-      }
-    }
+              // format as 'key=value' instead of 'key=Optional[value]'
+              stringHelper.add(method.getName(), fieldValueOptional.get());
+            } else {
+              // not an Optional value, keep as is
+              stringHelper.add(method.getName(), returnValue);
+            }
+          }
 
-    return stringHelper.toString();
+          return stringHelper.toString();
+        });
   }
 
   public static Metadata.Builder builder() {
diff --git a/java/com/google/gerrit/server/logging/TraceContext.java b/java/com/google/gerrit/server/logging/TraceContext.java
index 30c5250..21a4ce6 100644
--- a/java/com/google/gerrit/server/logging/TraceContext.java
+++ b/java/com/google/gerrit/server/logging/TraceContext.java
@@ -196,13 +196,13 @@
       this(
           () ->
               logger.atFine().log(
-                  "Starting timer for %s (%s)", operation, metadata.toStringForLogging()),
+                  "Starting timer for %s (%s)", operation, metadata.toStringForLoggingLazy()),
           elapsedMs -> {
             LoggingContext.getInstance()
                 .addPerformanceLogRecord(
                     () -> PerformanceLogRecord.create(operation, elapsedMs, metadata));
             logger.atFine().log(
-                "%s (%s) done (%d ms)", operation, metadata.toStringForLogging(), elapsedMs);
+                "%s (%s) done (%d ms)", operation, metadata.toStringForLoggingLazy(), elapsedMs);
           });
     }
 
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 9a2ecdd..694bb82 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -395,7 +395,7 @@
           withForce,
           projectControl.getProject().getName(),
           refName,
-          callerFinder.findCaller());
+          callerFinder.findCallerLazy());
       return false;
     }
 
@@ -408,7 +408,7 @@
             withForce,
             projectControl.getProject().getName(),
             refName,
-            callerFinder.findCaller());
+            callerFinder.findCallerLazy());
         return true;
       }
     }
@@ -420,7 +420,7 @@
         withForce,
         projectControl.getProject().getName(),
         refName,
-        callerFinder.findCaller());
+        callerFinder.findCallerLazy());
     return false;
   }
 
diff --git a/javatests/com/google/gerrit/server/logging/MetadataTest.java b/javatests/com/google/gerrit/server/logging/MetadataTest.java
index 89e5690..f9ae2c1 100644
--- a/javatests/com/google/gerrit/server/logging/MetadataTest.java
+++ b/javatests/com/google/gerrit/server/logging/MetadataTest.java
@@ -23,7 +23,7 @@
   @Test
   public void stringForLoggingOmitsEmptyOptionalValuesAndReformatsOptionalValuesThatArePresent() {
     Metadata metadata = Metadata.builder().accountId(1000001).branchName("refs/heads/foo").build();
-    assertThat(metadata.toStringForLogging())
+    assertThat(metadata.toStringForLoggingLazy().evaluate())
         .isEqualTo("Metadata{accountId=1000001, branchName=refs/heads/foo, pluginMetadata=[]}");
   }
 
@@ -31,6 +31,7 @@
   public void
       stringForLoggingOmitsEmptyOptionalValuesAndReformatsOptionalValuesThatArePresentNoFieldsSet() {
     Metadata metadata = Metadata.builder().build();
-    assertThat(metadata.toStringForLogging()).isEqualTo("Metadata{pluginMetadata=[]}");
+    assertThat(metadata.toStringForLoggingLazy().evaluate())
+        .isEqualTo("Metadata{pluginMetadata=[]}");
   }
 }