Reland "Optimize VlogInfo::GetVlogLevel"

This is a reland of 290a1c1f74bb9943d3673eeb6e6d2feb1b02577a

Prior to the reverted change, no synchronization was needed because we
only read VlogInfo::vmodule_levels_; it was never modified outside of
the constructor.

For this reland, I've added a base::Lock to guard the field. This is
necessary because one of the optimizations involves swapping the most
desirable values of vmodule_levels_ towards the front.

At this point in time, I was not quite able to reproduce the performance
improvement described in the original message.

Recomputing the stats over four runs on an official build, using the
same GN args:
- Before:             the perftest averages 10 gets/us (631ms total)
- With reverted code: the perftest averages 10 gets/us (551.25ms total)
- With base::Lock:    the perftest averages 11 gets/us (564.5ms total)

Percentage-wise, the before-and-after improvement now is 10.5%. Included
in that, adding the base::Lock was only a 2% penalty.

Original change's description:
> Optimize VlogInfo::GetVlogLevel
>
> On ChromeOS-Wide Profiling, I noticed that VlogInfo::GetVlogLevel takes
> up 0.78% of running time, which seems significant.
>
> This CL adds a perftest and the changes described below.
>
> 1. I believe that calls to GetVlogLevel have high locality. That is, the
>    function is likely to see calls from the same file near each other.
>    We can maintain the vector of patterns such that the
>    highest-probability patterns are near the front.
>
> 2. There's a bunch of places where we used references to StringPiece
>    instead of just plain values. This is an easy fix and it's
>    recommended by StringPiece's documentation.
>
> 3. The GetModule() helper function, which removes the file extension,
>    "-inl" suffix, and the path leading up to the file name, can be
>    reorganized to do less work on each of its right-to-left scans of the
>    input string. Specifically, we want to remove the file extension and
>    "-inl" suffix *before* scanning right-to-left for the last forward or
>    backward slash.
>
> Hastily-computed results on my Linux workstation, averaged over two
> runs on an official build:
>   - Before: the perftest averages 10 gets/us (609ms total).
>   - After: the perftest averages 13 gets/us (476ms total).
>
> That's an improvement of 22%! (Calculated as 1 - 476/609.)
>
> Aside: on my local builds with GN args "is_debug = false", I observed
> that redundant occurrences of CHECK_LT were not being optimized, but
> when converted to plain CHECK, they were correctly optimized. This
> stopped happening when I switched to an official build. Specifically,
> the bounds check in base::StringPiece::at was not being eliminated, even
> though MatchVlogPattern does its own bounds checking.
>
> GN args used:
>     is_debug = false
>     is_component_build = false
>     is_official_build = true
>     blink_symbol_level = 0
>     symbol_level = 0
>
> Change-Id: I226b4ea67fc46a1b075f02b7e47c0be073a40301
> Bug: 489441
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3046418
> Reviewed-by: Daniel Cheng <[email protected]>
> Commit-Queue: Dan McArdle <[email protected]>
> Cr-Commit-Position: refs/heads/master@{#906903}

Bug: 489441, 1238262
Change-Id: Iefd94c171db02e91ad3a01d5f49531ce1711ebdb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3086662
Reviewed-by: Daniel Cheng <[email protected]>
Commit-Queue: Dan McArdle <[email protected]>
Cr-Commit-Position: refs/heads/master@{#910601}
diff --git a/base/vlog.cc b/base/vlog.cc
index cdc30c3..a973b317 100644
--- a/base/vlog.cc
+++ b/base/vlog.cc
@@ -5,11 +5,11 @@
 #include "base/vlog.h"
 
 #include <stddef.h>
-
+#include <algorithm>
+#include <limits>
 #include <ostream>
 #include <utility>
 
-#include "base/cxx17_backports.h"
 #include "base/logging.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_split.h"
@@ -24,17 +24,16 @@
 
   explicit VmodulePattern(const std::string& pattern);
 
-  VmodulePattern();
+  VmodulePattern() = default;
 
   std::string pattern;
-  int vlog_level;
-  MatchTarget match_target;
+  int vlog_level = VlogInfo::kDefaultVlogLevel;
+  MatchTarget match_target = MATCH_MODULE;
+  size_t score = 0;
 };
 
 VlogInfo::VmodulePattern::VmodulePattern(const std::string& pattern)
-    : pattern(pattern),
-      vlog_level(VlogInfo::kDefaultVlogLevel),
-      match_target(MATCH_MODULE) {
+    : pattern(pattern) {
   // If the pattern contains a {forward,back} slash, we assume that
   // it's meant to be tested against the entire __FILE__ string.
   std::string::size_type first_slash = pattern.find_first_of("\\/");
@@ -42,10 +41,6 @@
     match_target = MATCH_FILE;
 }
 
-VlogInfo::VmodulePattern::VmodulePattern()
-    : vlog_level(VlogInfo::kDefaultVlogLevel),
-      match_target(MATCH_MODULE) {}
-
 VlogInfo::VlogInfo(const std::string& v_switch,
                    const std::string& vmodule_switch,
                    int* min_log_level)
@@ -86,31 +81,59 @@
 // Given a path, returns the basename with the extension chopped off
 // (and any -inl suffix).  We avoid using FilePath to minimize the
 // number of dependencies the logging system has.
-base::StringPiece GetModule(const base::StringPiece& file) {
-  base::StringPiece module(file);
-  base::StringPiece::size_type last_slash_pos =
-      module.find_last_of("\\/");
-  if (last_slash_pos != base::StringPiece::npos)
-    module.remove_prefix(last_slash_pos + 1);
+base::StringPiece GetModule(base::StringPiece file) {
+  base::StringPiece module = file;
+
+  // Chop off the file extension.
   base::StringPiece::size_type extension_start = module.rfind('.');
   module = module.substr(0, extension_start);
-  static const char kInlSuffix[] = "-inl";
-  static const int kInlSuffixLen = base::size(kInlSuffix) - 1;
+
+  // Chop off the -inl suffix.
+  static constexpr base::StringPiece kInlSuffix("-inl");
   if (base::EndsWith(module, kInlSuffix))
-    module.remove_suffix(kInlSuffixLen);
+    module.remove_suffix(kInlSuffix.size());
+
+  // Chop off the path up to the start of the file name. Using single-character
+  // overload of `base::StringPiece::find_last_of` for speed; this overload does
+  // not build a lookup table.
+  base::StringPiece::size_type last_slash_pos = module.find_last_of('/');
+  if (last_slash_pos != base::StringPiece::npos) {
+    module.remove_prefix(last_slash_pos + 1);
+    return module;
+  }
+  last_slash_pos = module.find_last_of('\\');
+  if (last_slash_pos != base::StringPiece::npos)
+    module.remove_prefix(last_slash_pos + 1);
   return module;
 }
 
 }  // namespace
 
-int VlogInfo::GetVlogLevel(const base::StringPiece& file) const {
+int VlogInfo::GetVlogLevel(base::StringPiece file) {
+  base::AutoLock lock(vmodule_levels_lock_);
   if (!vmodule_levels_.empty()) {
     base::StringPiece module(GetModule(file));
-    for (const auto& it : vmodule_levels_) {
-      base::StringPiece target(
-          (it.match_target == VmodulePattern::MATCH_FILE) ? file : module);
-      if (MatchVlogPattern(target, it.pattern))
-        return it.vlog_level;
+    for (size_t i = 0; i < vmodule_levels_.size(); i++) {
+      VmodulePattern& it = vmodule_levels_[i];
+
+      const bool kUseFile = it.match_target == VmodulePattern::MATCH_FILE;
+      if (!MatchVlogPattern(kUseFile ? file : module, it.pattern)) {
+        continue;
+      }
+      const int ret = it.vlog_level;
+
+      // Since `it` matched, increase its score because we believe it has a
+      // higher probability of winning next time.
+      if (it.score == std::numeric_limits<size_t>::max()) {
+        for (VmodulePattern& pattern : vmodule_levels_) {
+          pattern.score = 0;
+        }
+      }
+      ++it.score;
+      if (i > 0 && it.score > vmodule_levels_[i - 1].score)
+        std::swap(it, vmodule_levels_[i - 1]);
+
+      return ret;
     }
   }
   return GetMaxVlogLevel();
@@ -125,23 +148,20 @@
   return -*min_log_level_;
 }
 
-bool MatchVlogPattern(const base::StringPiece& string,
-                      const base::StringPiece& vlog_pattern) {
-  base::StringPiece pat(vlog_pattern);
-  base::StringPiece str(string);
-
+bool MatchVlogPattern(base::StringPiece string,
+                      base::StringPiece vlog_pattern) {
   // The code implements the glob matching using a greedy approach described in
   // https://research.swtch.com/glob.
   size_t s = 0, nexts = 0;
   size_t p = 0, nextp = 0;
-  size_t slen = str.size(), plen = pat.size();
+  const size_t slen = string.size(), plen = vlog_pattern.size();
   while (s < slen || p < plen) {
     if (p < plen) {
-      switch (pat[p]) {
+      switch (vlog_pattern[p]) {
         // A slash (forward or back) must match a slash (forward or back).
         case '/':
         case '\\':
-          if (s < slen && (str[s] == '/' || str[s] == '\\')) {
+          if (s < slen && (string[s] == '/' || string[s] == '\\')) {
             p++, s++;
             continue;
           }
@@ -160,7 +180,7 @@
           continue;
         // Anything else must match literally.
         default:
-          if (s < slen && str[s] == pat[p]) {
+          if (s < slen && string[s] == vlog_pattern[p]) {
             p++, s++;
             continue;
           }