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
next prev parent 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