diff options
author | Yabin Cui <yabinc@google.com> | 2024-01-29 14:19:13 -0800 |
---|---|---|
committer | Yabin Cui <yabinc@google.com> | 2024-01-29 15:04:59 -0800 |
commit | 73d797327bde7b64a3bafcfbc2f545aa461dba7d (patch) | |
tree | 0fd04dae8c6848339ce0b4831d9bdd50288d32ba /simpleperf | |
parent | 5ce0102ccd44e70b6d10e6f23d9b2e924081b4f1 (diff) | |
download | extras-73d797327bde7b64a3bafcfbc2f545aa461dba7d.tar.gz |
simpleperf: scripts: Add sample_filter.py
It is used to generate sample filter files. A filter file
can be passed via --filter-file in report scripts. For example,
when a recording file is too large to report, we can split it
into several reporting files using filter files:
./sample_filter.py -i perf.data --split-time-range 2 -o sample_filter
./gecko_profile_generator.py -i perf.data --filter-file sample_filter_part1 \
| gzip >profile-part1.json.gz
./gecko_profile_generator.py -i perf.data --filter-file sample_filter_part2 \
| gzip >profile-part2.json.gz
Bug: 322509778
Test: run test.py --only-host-test
Change-Id: Icea816154d8e21463776a0ce9c407917307d98e9
Diffstat (limited to 'simpleperf')
-rw-r--r-- | simpleperf/doc/sample_filter.md | 3 | ||||
-rw-r--r-- | simpleperf/doc/scripts_reference.md | 15 | ||||
-rwxr-xr-x | simpleperf/scripts/sample_filter.py | 130 | ||||
-rwxr-xr-x | simpleperf/scripts/test/do_test.py | 2 | ||||
-rw-r--r-- | simpleperf/scripts/test/sample_filter_test.py | 42 |
5 files changed, 191 insertions, 1 deletions
diff --git a/simpleperf/doc/sample_filter.md b/simpleperf/doc/sample_filter.md index 3755e760..421c9758 100644 --- a/simpleperf/doc/sample_filter.md +++ b/simpleperf/doc/sample_filter.md @@ -7,7 +7,8 @@ ranges. To filter samples, we can pass filter options to the report commands or ## filter file format To filter samples based on time ranges, simpleperf accepts a filter file when reporting. The filter -file is in text format, containing a list of lines. Each line is a filter command. +file is in text format, containing a list of lines. Each line is a filter command. The filter file +can be generated by `sample_filter.py`, and passed to report scripts via `--filter-file`. ``` filter_command1 command_args diff --git a/simpleperf/doc/scripts_reference.md b/simpleperf/doc/scripts_reference.md index fd76ed94..d118ed2c 100644 --- a/simpleperf/doc/scripts_reference.md +++ b/simpleperf/doc/scripts_reference.md @@ -340,3 +340,18 @@ K_CYCLES K_INSTR IPC 104562 41350 0.40 138264 54916 0.40 ``` + +## sample_filter.py + +`sample_filter.py` generates sample filter files as documented in [sample_filter.md](https://android.googlesource.com/platform/system/extras/+/refs/heads/main/simpleperf/doc/sample_filter.md). +A filter file can be passed in `--filter-file` when running report scripts. + +For example, it can be used to split a large recording file into several report files. + +```sh +$ sample_filter.py -i perf.data --split-time-range 2 -o sample_filter +$ gecko_profile_generator.py -i perf.data --filter-file sample_filter_part1 \ + | gzip >profile-part1.json.gz +$ gecko_profile_generator.py -i perf.data --filter-file sample_filter_part2 \ + | gzip >profile-part2.json.gz +``` diff --git a/simpleperf/scripts/sample_filter.py b/simpleperf/scripts/sample_filter.py new file mode 100755 index 00000000..c3617629 --- /dev/null +++ b/simpleperf/scripts/sample_filter.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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. +# + +"""sample_filter.py: generate sample filter files, which can be passed in the + --filter-file option when reporting. + +Example: + ./sample_filter.py -i perf.data --split-time-range 2 -o sample_filter + ./gecko_profile_generator.py -i perf.data --filter-file sample_filter_part1 \ + | gzip >profile-part1.json.gz + ./gecko_profile_generator.py -i perf.data --filter-file sample_filter_part2 \ + | gzip >profile-part2.json.gz +""" + +import logging +from simpleperf_report_lib import ReportLib +from simpleperf_utils import BaseArgumentParser +from typing import Tuple + + +class RecordFileReader: + def __init__(self, record_file: str): + self.record_file = record_file + + def get_time_range(self) -> Tuple[int, int]: + """ Return a tuple of (min_timestamp, max_timestamp). """ + min_timestamp = 0 + max_timestamp = 0 + lib = ReportLib() + lib.SetRecordFile(self.record_file) + while True: + sample = lib.GetNextSample() + if not sample: + break + if not min_timestamp or sample.time < min_timestamp: + min_timestamp = sample.time + if not max_timestamp or sample.time > max_timestamp: + max_timestamp = sample.time + lib.Close() + return (min_timestamp, max_timestamp) + + +def show_time_range(record_file: str) -> None: + reader = RecordFileReader(record_file) + time_range = reader.get_time_range() + print('time range of samples is %.3f s' % ((time_range[1] - time_range[0]) / 1e9)) + + +def filter_samples( + record_file: str, split_time_range: int, exclude_first_seconds: int, + exclude_last_seconds: int, output_file_prefix: str) -> None: + reader = RecordFileReader(record_file) + min_timestamp, max_timestamp = reader.get_time_range() + comment = 'total time range: %d seconds' % ((max_timestamp - min_timestamp) // 1e9) + if exclude_first_seconds: + min_timestamp += int(exclude_first_seconds * 1e9) + comment += ', exclude first %d seconds' % exclude_first_seconds + if exclude_last_seconds: + max_timestamp -= int(exclude_last_seconds * 1e9) + comment += ', exclude last %d seconds' % exclude_last_seconds + if min_timestamp > max_timestamp: + logging.error('All samples are filtered out') + return + if not split_time_range: + output_file = output_file_prefix + with open(output_file, 'w') as fh: + fh.write('// %s\n' % comment) + fh.write('GLOBAL_BEGIN %d\n' % min_timestamp) + fh.write('GLOBAL_END %d\n' % max_timestamp) + print('Generate sample filter file: %s' % output_file) + else: + step = (max_timestamp - min_timestamp) // split_time_range + cur_timestamp = min_timestamp + for i in range(split_time_range): + output_file = output_file_prefix + '_part%s' % (i + 1) + with open(output_file, 'w') as fh: + time_range_comment = 'current range: %d to %d seconds' % ( + (cur_timestamp - min_timestamp) // 1e9, + (cur_timestamp + step - min_timestamp) // 1e9) + fh.write('// %s, %s\n' % (comment, time_range_comment)) + fh.write('GLOBAL_BEGIN %d\n' % cur_timestamp) + if i == split_time_range - 1: + cur_timestamp = max_timestamp + else: + cur_timestamp += step + fh.write('GLOBAL_END %d\n' % (cur_timestamp + 1)) + cur_timestamp += 1 + print('Generate sample filter file: %s' % output_file) + + +def main(): + parser = BaseArgumentParser(description=__doc__) + parser.add_argument('-i', '--record-file', nargs='?', default='perf.data', + help='Default is perf.data.') + parser.add_argument('--show-time-range', action='store_true', help='show time range of samples') + parser.add_argument('--split-time-range', type=int, + help='split time ranges of samples into several parts') + parser.add_argument('--exclude-first-seconds', type=int, + help='exclude samples recorded in the first seconds') + parser.add_argument('--exclude-last-seconds', type=int, + help='exclude samples recorded in the last seconds') + parser.add_argument( + '-o', '--output-file-prefix', default='sample_filter', + help='prefix for the generated sample filter files') + args = parser.parse_args() + + if args.show_time_range: + show_time_range(args.record_file) + + if args.split_time_range or args.exclude_first_seconds or args.exclude_last_seconds: + filter_samples(args.record_file, args.split_time_range, args.exclude_first_seconds, + args.exclude_last_seconds, args.output_file_prefix) + + +if __name__ == '__main__': + main() diff --git a/simpleperf/scripts/test/do_test.py b/simpleperf/scripts/test/do_test.py index 83168b03..d341c20b 100755 --- a/simpleperf/scripts/test/do_test.py +++ b/simpleperf/scripts/test/do_test.py @@ -59,6 +59,7 @@ from . report_html_test import * from . report_lib_test import * from . report_sample_test import * from . run_simpleperf_on_device_test import * +from . sample_filter_test import * from . stackcollapse_test import * from . tools_test import * from . test_utils import TestHelper @@ -135,6 +136,7 @@ def get_test_type(test: str) -> Optional[str]: 'TestReportHtml', 'TestReportLib', 'TestReportSample', + 'TestSampleFilter', 'TestStackCollapse', 'TestTools', 'TestGeckoProfileGenerator'): diff --git a/simpleperf/scripts/test/sample_filter_test.py b/simpleperf/scripts/test/sample_filter_test.py new file mode 100644 index 00000000..46d51d08 --- /dev/null +++ b/simpleperf/scripts/test/sample_filter_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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. + +import json +import os +from pathlib import Path +import re +import tempfile +from typing import List, Optional, Set + +from . test_utils import TestBase, TestHelper + + +class TestSampleFilter(TestBase): + def test_show_time_range(self): + testdata_file = TestHelper.testdata_path('perf_display_bitmaps.data') + output = self.run_cmd(['sample_filter.py', '-i', testdata_file, + '--show-time-range'], return_output=True) + self.assertIn('0.134 s', output) + + def test_split_time_range(self): + testdata_file = TestHelper.testdata_path('perf_display_bitmaps.data') + self.run_cmd(['sample_filter.py', '-i', testdata_file, '--split-time-range', '2']) + part1_data = Path('sample_filter_part1').read_text() + self.assertIn('GLOBAL_BEGIN 684943449406175', part1_data) + self.assertIn('GLOBAL_END 684943516618526', part1_data) + part2_data = Path('sample_filter_part2').read_text() + self.assertIn('GLOBAL_BEGIN 684943516618526', part2_data) + self.assertIn('GLOBAL_END 684943583830876', part2_data) |