Linux Perf Users
 help / color / mirror / Atom feed
From: Ian Rogers <irogers@google.com>
To: Peter Zijlstra <peterz@infradead.org>,
	Ingo Molnar <mingo@redhat.com>,
	 Arnaldo Carvalho de Melo <acme@kernel.org>,
	Namhyung Kim <namhyung@kernel.org>
Cc: Jiri Olsa <jolsa@kernel.org>,
	Adrian Hunter <adrian.hunter@intel.com>,
	 James Clark <james.clark@linaro.org>,
	Kan Liang <kan.liang@linux.intel.com>,
	 linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
	 Ian Rogers <irogers@google.com>
Subject: [PATCH 01/10] perf jevents: Store raw bytes in BigCString instead of C-escaped strings
Date: Fri, 13 Nov 2026 13:08:13 -0800	[thread overview]
Message-ID: <20261113210822.3112700-2-irogers@google.com> (raw)
In-Reply-To: <20261113210822.3112700-1-irogers@google.com>

Previously BigCString stored C-escaped strings (with literal '\000',
'\n', '\"', '\\') and estimated their compiled byte length via
c_len(), which did not handle arbitrary escapes and caused both suffix
folding and metricgroups sorting to compare '\000' ('\\' == 0x5c)
rather than NUL (0x00). Notably, sorting metricgroup names with '\000'
placed 'BadSpec\000' ('S' == 0x53) before 'Bad\000' ('\\' == 0x5c),
breaking strcmp() binary search in describe_metricgroup("Bad").

Store raw NUL-delimited UTF-8 bytes directly in BigCString and
_metricgroups, compute offsets from len(b) directly, remove c_len(), and
escape bytes into C string literals only when emitting C source via
bytes_to_c_literal().

Fixes: 66c6e0c10027 ("perf jevents: Add support for metricgroup descriptions")
Signed-off-by: Ian Rogers <irogers@google.com>
---
 tools/perf/pmu-events/jevents.py | 151 +++++++++++++++----------------
 1 file changed, 74 insertions(+), 77 deletions(-)

diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 080e04b51b8c..0721c8b2551c 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -37,7 +37,7 @@ _pending_metrics_tblname: Optional[str] = None
 # Global BigCString shared by all structures.
 _bcs: "BigCString"
 # Map from the name of a metric group to a description of the group.
-_metricgroups: Dict[str, str] = {}
+_metricgroups: Dict[bytes, bytes] = {}
 # Order specific JsonEvent attributes will be visited.
 _json_event_attributes = [
     # cmp_sevent related attributes.
@@ -74,26 +74,24 @@ def file_name_to_table_name(prefix: str, parents: Sequence[str],
   return tblname.replace('-', '_')
 
 
-def c_len(s: str) -> int:
-  """Return the length of s a C string
+_C_ESCAPE_TABLE = str.maketrans({
+    '\\': '\\\\',
+    '"': '\\"',
+    '\n': '\\n',
+    '\r': '\\r',
+    '\t': '\\t',
+    **{chr(c): f'\\{c:03o}' for c in range(32) if chr(c) not in ('\n', '\r', '\t')},
+    chr(127): '\\177',
+})
+
+
+def bytes_to_c_literal(b: bytes) -> str:
+  """Convert UTF-8 bytes (with NUL terminators) to a C string literal body."""
+  return b.decode('utf-8').translate(_C_ESCAPE_TABLE)
 
-  This doesn't handle all escape characters properly. It first assumes
-  all \\ are for escaping, it then adjusts as it will have over counted
-  \\. The code uses \000 rather than \0 as a terminator as an adjacent
-  number would be folded into a string of \0 (ie. "\0" + "5" doesn't
-  equal a terminator followed by the number 5 but the escape of
-  \05). The code adjusts for \000 but not properly for all octal, hex
-  or unicode values.
-  """
-  try:
-    utf = s.encode(encoding='utf-8',errors='strict')
-  except:
-    print(f'broken string {s}')
-    raise
-  return len(utf) - utf.count(b'\\') + utf.count(b'\\\\') - (utf.count(b'\\000') * 2)
 
 class BigCString:
-  """A class to hold many strings concatenated together.
+  """A class to hold many byte strings concatenated together.
 
   Generating a large number of stand-alone C strings creates a large
   number of relocations in position independent code. The BigCString
@@ -105,12 +103,12 @@ class BigCString:
   strings are merged. If a longer string ends-with the same value as a
   shorter string, these entries are also merged.
   """
-  strings: Set[str]
+  strings: Set[bytes]
   big_string: Sequence[str]
-  offsets: Dict[str, int]
+  offsets: Dict[bytes, int]
   insert_number: int
-  insert_point: Dict[str, int]
-  metrics: Set[str]
+  insert_point: Dict[bytes, int]
+  metrics: Set[bytes]
 
   def __init__(self):
     self.strings = set()
@@ -118,21 +116,21 @@ class BigCString:
     self.insert_point = {}
     self.metrics = set()
 
-  def add(self, s: str, is_metric: bool) -> None:
-    """Called to add to the big string."""
-    if s not in self.strings:
-      self.strings.add(s)
-      self.insert_point[s] = self.insert_number
+  def add(self, b: bytes, is_metric: bool) -> None:
+    """Called to add NUL-terminated UTF-8 bytes to the big string."""
+    if b not in self.strings:
+      self.strings.add(b)
+      self.insert_point[b] = self.insert_number
       self.insert_number += 1
       if is_metric:
-        self.metrics.add(s)
+        self.metrics.add(b)
 
   def compute(self) -> None:
     """Called once all strings are added to compute the string and offsets."""
 
-    folded_strings = {}
+    folded_strings: Dict[bytes, bytes] = {}
     # Determine if two strings can be folded, ie. let 1 string use the
-    # end of another. First reverse all strings and sort them.
+    # end of another. First reverse all byte strings and sort them.
     sorted_reversed_strings = sorted([x[::-1] for x in self.strings])
 
     # Strings 'xyz' and 'yz' will now be [ 'zy', 'zyx' ]. Scan forward
@@ -140,16 +138,16 @@ class BigCString:
     # into, in the example rather than using 'yz' we can use'xyz' at
     # an offset of 1. We record which string can be folded into which
     # in folded_strings, we don't need to record the offset as it is
-    # trivially computed from the string lengths.
-    for pos,s in enumerate(sorted_reversed_strings):
+    # trivially computed from the byte lengths.
+    for pos, b in enumerate(sorted_reversed_strings):
       best_pos = pos
       for check_pos in range(pos + 1, len(sorted_reversed_strings)):
-        if sorted_reversed_strings[check_pos].startswith(s):
+        if sorted_reversed_strings[check_pos].startswith(b):
           best_pos = check_pos
         else:
           break
       if pos != best_pos:
-        folded_strings[s[::-1]] = sorted_reversed_strings[best_pos][::-1]
+        folded_strings[b[::-1]] = sorted_reversed_strings[best_pos][::-1]
 
     # Compute reverse mappings for debugging.
     fold_into_strings = collections.defaultdict(set)
@@ -157,36 +155,34 @@ class BigCString:
       if key != val:
         fold_into_strings[val].add(key)
 
-    # big_string_offset is the current location within the C string
+    # big_string_offset is the current byte location within the C string
     # being appended to - comments, etc. don't count. big_string is
-    # the string contents represented as a list. Strings are immutable
-    # in Python and so appending to one causes memory issues, while
-    # lists are mutable.
+    # the C string literal contents represented as a list.
     big_string_offset = 0
     self.big_string = []
     self.offsets = {}
 
-    def string_cmp_key(s: str) -> Tuple[bool, int, str]:
-      return (s in self.metrics, self.insert_point[s], s)
+    def string_cmp_key(b: bytes) -> Tuple[bool, int, bytes]:
+      return (b in self.metrics, self.insert_point[b], b)
 
     # Emit all strings that aren't folded in a sorted manner.
-    for s in sorted(self.strings, key=string_cmp_key):
-      if s not in folded_strings:
-        self.offsets[s] = big_string_offset
+    for b in sorted(self.strings, key=string_cmp_key):
+      if b not in folded_strings:
+        self.offsets[b] = big_string_offset
         self.big_string.append(f'/* offset={big_string_offset} */\n"')
-        self.big_string.append(s)
+        self.big_string.append(bytes_to_c_literal(b))
         self.big_string.append('"')
-        if s in fold_into_strings:
-          self.big_string.append(' /* also: ' + ', '.join(fold_into_strings[s]) + ' */')
+        if b in fold_into_strings:
+          also = [bytes_to_c_literal(x).replace('*/', r'\*\/')
+                  for x in sorted(fold_into_strings[b])]
+          self.big_string.append(' /* also: ' + ', '.join(also) + ' */')
         self.big_string.append('\n')
-        big_string_offset += c_len(s)
-        continue
+        big_string_offset += len(b)
 
     # Compute the offsets of the folded strings.
-    for s in folded_strings.keys():
-      assert s not in self.offsets
-      folded_s = folded_strings[s]
-      self.offsets[s] = self.offsets[folded_s] + c_len(folded_s) - c_len(s)
+    for b, folded_b in folded_strings.items():
+      assert b not in self.offsets
+      self.offsets[b] = self.offsets[folded_b] + len(folded_b) - len(b)
 
 _bcs = BigCString()
 
@@ -204,8 +200,7 @@ class JsonEvent:
       """Fix formatting issue for the desc string."""
       if s is None:
         return None
-      return s.removesuffix('.  ').removesuffix('. ').removesuffix('.').replace(
-          '\n', '\\n').replace('\"', '\\"').replace('\r', '\\r')
+      return s.removesuffix('.  ').removesuffix('. ').removesuffix('.')
 
     def convert_aggr_mode(aggr_mode: Optional[str]) -> Optional[str]:
       """Returns the aggr_mode_class enum value associated with the JSON string."""
@@ -425,21 +420,19 @@ class JsonEvent:
         s += f'\t{attr} = {value},\n'
     return s + '}'
 
-  def build_c_string(self, is_metric: bool) -> str:
+  def build_bytes(self, is_metric: bool) -> bytes:
+    """Encode the event or metric attributes as NUL-delimited UTF-8 bytes."""
     s = ''
     for attr in _json_metric_attributes if is_metric else _json_event_attributes:
       x = getattr(self, attr)
       if is_metric and x and attr == 'metric_expr':
-        # Convert parsed metric expressions into a string. Slashes
-        # must be doubled in the file.
-        x = x.ToPerfJson().replace('\\', '\\\\')
-      if is_metric and x and attr == 'metric_threshold':
-        x = x.replace('\\', '\\\\')
+        # Convert parsed metric expressions into a string.
+        x = x.ToPerfJson()
       if attr in _json_enum_attributes:
         s += x if x else '0'
       else:
-        s += f'{x}\\000' if x else '\\000'
-    return s
+        s += f'{x}\0' if x else '\0'
+    return s.encode('utf-8')
 
   def to_c_string(self, is_metric: bool) -> str:
     """Representation of the event as a C struct initializer."""
@@ -448,9 +441,10 @@ class JsonEvent:
         s = s.replace('*/', r'\*\/')
         return f'\t/* {s} */\n' if len(s) < 80 else f'\t/* {s[0:80]}... */\n'
 
-    s = self.build_c_string(is_metric)
+    b = self.build_bytes(is_metric)
+    s = bytes_to_c_literal(b)
     assert _bcs is not None
-    return f'{make_comment(s)}\t{{ { _bcs.offsets[s] } }},\n'
+    return f'{make_comment(s)}\t{{ { _bcs.offsets[b] } }},\n'
 
 
 _json_cache: Dict[Tuple[str, str], Sequence[JsonEvent]] = {}
@@ -556,11 +550,12 @@ def print_pending_events() -> None:
 static const struct pmu_table_entry {_pending_events_tblname}[] = {{
 """)
   for (pmu, tbl_pmu) in sorted(pmus):
-    pmu_name = f"{pmu}\\000"
+    pmu_name = (pmu + '\0').encode('utf-8')
+    pmu_comment = bytes_to_c_literal(pmu_name).replace('*/', r'\*\/')
     _args.output_file.write(f"""\t{{
 \t\t.entries = {_pending_events_tblname}_{tbl_pmu},
 \t\t.num_entries = ARRAY_SIZE({_pending_events_tblname}_{tbl_pmu}),
-\t\t.pmu_name = {{ {_bcs.offsets[pmu_name]} /* {pmu_name} */ }},
+\t\t.pmu_name = {{ {_bcs.offsets[pmu_name]} /* {pmu_comment} */ }},
 \t}},
 """)
   _args.output_file.write('};\n\n')
@@ -612,11 +607,12 @@ def print_pending_metrics() -> None:
 static const struct pmu_table_entry {_pending_metrics_tblname}[] = {{
 """)
   for (pmu, tbl_pmu) in sorted(pmus):
-    pmu_name = f"{pmu}\\000"
+    pmu_name = (pmu + '\0').encode('utf-8')
+    pmu_comment = bytes_to_c_literal(pmu_name).replace('*/', r'\*\/')
     _args.output_file.write(f"""\t{{
 \t\t.entries = {_pending_metrics_tblname}_{tbl_pmu},
 \t\t.num_entries = ARRAY_SIZE({_pending_metrics_tblname}_{tbl_pmu}),
-\t\t.pmu_name = {{ {_bcs.offsets[pmu_name]} /* {pmu_name} */ }},
+\t\t.pmu_name = {{ {_bcs.offsets[pmu_name]} /* {pmu_comment} */ }},
 \t}},
 """)
   _args.output_file.write('};\n\n')
@@ -646,23 +642,22 @@ def preprocess_one_file(parents: Sequence[str], item: os.DirEntry) -> None:
       metricgroup_descriptions = json.load(f)
     for mgroup in metricgroup_descriptions:
       assert len(mgroup) > 1, parents
-      description = f"{metricgroup_descriptions[mgroup]}\\000"
-      mgroup = f"{mgroup}\\000"
-      _bcs.add(mgroup, is_metric=True)
+      description = (metricgroup_descriptions[mgroup] + '\0').encode('utf-8')
+      mgroup_bytes = (mgroup + '\0').encode('utf-8')
+      _bcs.add(mgroup_bytes, is_metric=True)
       _bcs.add(description, is_metric=True)
-      _metricgroups[mgroup] = description
+      _metricgroups[mgroup_bytes] = description
     return
 
   topic = get_topic(item.name)
   for event in read_json_events(item.path, topic):
-    pmu_name = f"{event.pmu}\\000"
+    pmu_name = (event.pmu + '\0').encode('utf-8')
     if event.name:
       _bcs.add(pmu_name, is_metric=False)
-      _bcs.add(event.build_c_string(is_metric=False), is_metric=False)
+      _bcs.add(event.build_bytes(is_metric=False), is_metric=False)
     if event.metric_name:
-      pmu_name = f"{event.pmu}\\000"
       _bcs.add(pmu_name, is_metric=True)
-      _bcs.add(event.build_c_string(is_metric=True), is_metric=True)
+      _bcs.add(event.build_bytes(is_metric=True), is_metric=True)
 
 def process_one_file(parents: Sequence[str], item: os.DirEntry) -> None:
   """Process a JSON file during the main walk."""
@@ -1409,8 +1404,10 @@ static const int metricgroups[][2] = {
 """)
   for mgroup in sorted(_metricgroups):
     description = _metricgroups[mgroup]
+    mgroup_comment = bytes_to_c_literal(mgroup).replace('*/', r'\*\/')
+    desc_comment = bytes_to_c_literal(description).replace('*/', r'\*\/')
     _args.output_file.write(
-        f'\t{{ {_bcs.offsets[mgroup]}, {_bcs.offsets[description]} }}, /* {mgroup} => {description} */\n'
+        f'\t{{ {_bcs.offsets[mgroup]}, {_bcs.offsets[description]} }}, /* {mgroup_comment} => {desc_comment} */\n'
     )
   _args.output_file.write("""
 };

base-commit: 4af65363b11fc4fe65fcb497f68bbe4f3e3a4d2c
-- 
2.56.1.703.gf848ed807c-goog


  reply	other threads:[~2026-11-13 21:08 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-13 21:08 [PATCH 00/10] perf jevents: Block-indexed string compression and default JEVENTS_ARCH=all Ian Rogers
2026-11-13 21:08 ` Ian Rogers [this message]
2026-11-13 21:21   ` [PATCH 01/10] perf jevents: Store raw bytes in BigCString instead of C-escaped strings sashiko-bot
2026-11-13 21:08 ` [PATCH 02/10] perf jevents: Sort BigCString entries alphabetically Ian Rogers
2026-11-13 21:12   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 03/10] perf jevents: Store PMU names as string literals in struct pmu_table_entry Ian Rogers
2026-11-13 21:13   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 04/10] perf jevents: Encapsulate big_c_string lookups behind pmu_events__get_string() Ian Rogers
2026-11-13 21:12   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 05/10] perf jevents: Add block-indexed string packing and --compress=zlib Ian Rogers
2026-11-13 21:16   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 06/10] perf jevents: Add --compress=lzma block compression backend Ian Rogers
2026-11-13 21:13   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 07/10] perf jevents: Add --compress=zstd " Ian Rogers
2026-11-13 21:13   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 08/10] perf build: Enable jevents string compression when available Ian Rogers
2026-11-13 21:17   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 09/10] perf tests pmu-events: Test metricgroup descriptions and concurrent lookups Ian Rogers
2026-11-13 21:20   ` sashiko-bot
2026-11-13 21:08 ` [PATCH 10/10] perf build: Default JEVENTS_ARCH to all Ian Rogers
2026-11-13 21:21   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261113210822.3112700-2-irogers@google.com \
    --to=irogers@google.com \
    --cc=acme@kernel.org \
    --cc=adrian.hunter@intel.com \
    --cc=james.clark@linaro.org \
    --cc=jolsa@kernel.org \
    --cc=kan.liang@linux.intel.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=mingo@redhat.com \
    --cc=namhyung@kernel.org \
    --cc=peterz@infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox