summaryrefslogtreecommitdiffstats
path: root/mozglue/baseprofiler/build/generate_profiling_categories.py
blob: 64da056bbd6fde35e3c76302d8eff363b9dffda8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This script generates ProfilingCategoryList.h and profiling_categories.rs
# files from profiling_categories.yaml.

from collections import OrderedDict

import yaml

CPP_HEADER_TEMPLATE = """\
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef {includeguard}
#define {includeguard}

/* This file is generated by generate_profiling_categories.py from
   profiling_categories.yaml. DO NOT EDIT! */

// Profiler sub-categories are applied to each sampled stack to describe the
// type of workload that the CPU is busy with. Only one sub-category can be
// assigned so be mindful that these are non-overlapping. The active category is
// set by pushing a label to the profiling stack, or by the unwinder in cases
// such as JITs. A profile sample in arbitrary C++/Rust will typically be
// categorized based on the top of the label stack.
//
// The list of available color names for categories is:
//    transparent
//    blue
//    green
//    grey
//    lightblue
//    magenta
//    orange
//    purple
//    yellow

// clang-format off

{contents}

// clang-format on

#endif // {includeguard}
"""

CPP_MACRO_DEFINITION = """\
#define MOZ_PROFILING_CATEGORY_LIST(BEGIN_CATEGORY, SUBCATEGORY, END_CATEGORY) \\
"""

RUST_TEMPLATE = """\
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* This file is generated by generate_profiling_categories.py from
   profiling_categories.yaml. DO NOT EDIT! */

{contents}\
"""

RUST_ENUM_TEMPLATE = """\
#[repr(u32)]
#[derive(Debug, Copy, Clone)]
pub enum {name} {{
{fields}
}}
"""

RUST_CONVERSION_IMPL_TEMPLATE = """\
impl {name} {{
  pub fn to_cpp_enum_value(&self) -> u32 {{
{content}
  }}
}}
"""

RUST_DEFAULT_IMPL_TEMPLATE = """\
impl Default for {name} {{
  fn default() -> Self {{
{content}
  }}
}}
"""

RUST_MATCH_SELF = """\
    match *self {{
{fields}
    }}
"""


def generate_header(c_out, includeguard, contents):
    c_out.write(
        CPP_HEADER_TEMPLATE.format(includeguard=includeguard, contents=contents)
    )


def generate_rust_file(c_out, contents):
    c_out.write(RUST_TEMPLATE.format(contents=contents))


def load_yaml(yaml_path):
    # Load into an OrderedDict to ensure order is preserved. Note: Python 3.7+
    # also preserves ordering for normal dictionaries.
    # Code based on https://stackoverflow.com/a/21912744.
    class OrderedLoader(yaml.Loader):
        pass

    def construct_mapping(loader, node):
        loader.flatten_mapping(node)
        return OrderedDict(loader.construct_pairs(node))

    tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
    OrderedLoader.add_constructor(tag, construct_mapping)

    file_handler = open(yaml_path)
    return yaml.load(file_handler, OrderedLoader)


def generate_category_macro(name, label, color, subcategories):
    contents = '  BEGIN_CATEGORY({name}, "{label}", "{color}") \\\n'.format(
        name=name, label=label, color=color
    )

    subcategory_items = []

    for subcategory in subcategories:
        subcat_name = subcategory["name"]
        assert isinstance(subcat_name, str)
        subcat_label = subcategory["label"]
        assert isinstance(subcat_label, str)

        subcategory_items.append(
            '    SUBCATEGORY({parent_cat}, {name}, "{label}") \\\n'.format(
                parent_cat=name, name=subcat_name, label=subcat_label
            )
        )

    contents += "".join(subcategory_items)
    contents += "  END_CATEGORY"

    return contents


def generate_macro_header(c_out, yaml_path):
    """Generate ProfilingCategoryList.h from profiling_categories.yaml.
    The generated file has a macro to generate the profiling category enums.
    """

    data = load_yaml(yaml_path)

    # Stores the macro definition of each categories.
    category_items = []

    for category in data:
        name = category["name"]
        assert isinstance(name, str)
        label = category["label"]
        assert isinstance(label, str)
        color = category["color"]
        assert isinstance(color, str)
        subcategories = category.get("subcategories", None)
        assert (
            isinstance(subcategories, list) and len(subcategories) > 0
        ), "At least one subcategory expected as default in {}.".format(name)

        category_items.append(
            generate_category_macro(name, label, color, subcategories)
        )

    contents = CPP_MACRO_DEFINITION
    contents += " \\\n".join(category_items)

    generate_header(c_out, "baseprofiler_ProfilingCategoryList_h", contents)


class RustEnum:
    """Class that keeps the rust enum fields and impls.
    This is used for generating the Rust ProfilingCategoryPair and ProfilingCategory
    enums as well as ProfilingCategoryPair's sub category enums.
    For example, this can either generate an enum with discrimant fields for sub
    category enums and ProfilingCategory:
    ```
    #[repr(u32)]
    #[derive(Debug, Copy, Clone)]
    pub enum Graphics {
        LayerBuilding = 0,
        ...
    }
    ```
    or can generate an enum with optional tuple values for ProfilingCategoryPair
     to explicitly mention their sub categories:
    ```
    #[repr(u32)]
    #[derive(Debug, Copy, Clone)]
    pub enum ProfilingCategoryPair {
        Network(Option<Network>),
        ...
    }
    ```

    And in addition to enums, it will generate impls for each enum. See one
    example below:
    ```
    impl Default for Network {
        fn default() -> Self {
            Network::Other
        }
    }
    ```
    """

    def __init__(self, name):
        # Name of the Rust enum.
        self.name = name
        # Fields of the Rust enum. This list contains elements of
        # (field_name, field_string) tuple for convenience.
        self.fields = []
        # Impls of the Rust enum. Each element is a string.
        self.impls = []
        # Default category of the Rust enum. Main enums won't have it, but all
        # sub category enums must have one. This is being checked later.
        self.default_category = None

    def append_optional_tuple_field(self, field_name):
        """Append the enum fields list with an optional tuple field."""
        field = (field_name, "  {name}(Option<{name}>),".format(name=field_name))
        self.fields.append(field)

    def append_discriminant_field(self, field_name, field_value):
        """Append the enum fields list with a discriminant field."""
        field = (
            field_name,
            "  {name} = {value},".format(name=field_name, value=field_value),
        )
        self.fields.append(field)

    def append_default_impl(self, default_category):
        """Append the enum impls list with a default implementation."""
        self.default_category = default_category

        self.impls.append(
            RUST_DEFAULT_IMPL_TEMPLATE.format(
                name=self.name,
                content="      {category}::{subcategory}".format(
                    category=self.name, subcategory=self.default_category
                ),
            )
        )

    def append_conversion_impl(self, content):
        """Append the enum impls list with a conversion implementation for cpp values."""
        self.impls.append(
            RUST_CONVERSION_IMPL_TEMPLATE.format(name=self.name, content=content)
        )

    def to_rust_string(self):
        """Serialize the enum with its impls as a string"""
        joined_fields = "\n".join(map(lambda field: field[1], self.fields))
        result = RUST_ENUM_TEMPLATE.format(name=self.name, fields=joined_fields)
        result += "\n"
        result += "\n".join(self.impls)
        return result


def generate_rust_enums(c_out, yaml_path):
    """Generate profiling_categories.rs from profiling_categories.yaml.
    The generated file has a profiling category enums and their impls.
    """

    data = load_yaml(yaml_path)

    # Each category has its own enum for keeping its subcategories. We are
    # keeping all of them here.
    enums = []
    # Parent enums for prifiling category and profiling category pair. They will
    # be appended to the end of the `enums`.
    profiling_category_pair_enum = RustEnum("ProfilingCategoryPair")
    profiling_category_enum = RustEnum("ProfilingCategory")
    profiling_category_pair_value = 0

    for cat_index, category in enumerate(data):
        cat_name = category["name"]
        assert isinstance(cat_name, str)
        cat_label = category["label"]
        assert isinstance(cat_label, str)
        # This will be used as our main enum field and sub category enum.
        cat_label = "".join(filter(str.isalnum, cat_label))
        cat_subcategories = category.get("subcategories", None)
        assert (
            isinstance(cat_subcategories, list) and len(cat_subcategories) > 0
        ), "At least one subcategory expected as default in {}.".format(cat_name)

        # Create a new enum for this sub category and append it to the enums list.
        category_enum = RustEnum(cat_label)
        enums.append(category_enum)

        for subcategory in cat_subcategories:
            subcat_name = subcategory["name"]
            assert isinstance(subcat_name, str)
            subcat_label = subcategory["label"]
            assert isinstance(subcat_label, str)
            friendly_subcat_name = None

            if cat_name == subcat_name:
                # This is the default sub-category. It should use the label as name.
                friendly_subcat_name = subcat_label
                category_enum.append_default_impl(subcat_label)
            else:
                # This is a non-default sub-category.
                underscore_pos = subcat_name.find("_")
                friendly_subcat_name = subcat_name[underscore_pos + 1 :]

            friendly_subcat_name = "".join(filter(str.isalnum, friendly_subcat_name))
            category_enum.append_discriminant_field(
                friendly_subcat_name, profiling_category_pair_value
            )
            profiling_category_pair_value += 1

        assert (
            category_enum.default_category is not None
        ), "There must be a default subcategory with the same name."

        # Append the main enums.
        profiling_category_pair_enum.append_optional_tuple_field(cat_label)
        profiling_category_enum.append_discriminant_field(cat_label, cat_index)

    # Add the main enums impls for conversion into cpp values.
    profiling_category_pair_impl_fields = "\n".join(
        "      {enum_name}::{field_name}(val) => val.unwrap_or_default() as u32,".format(
            enum_name="ProfilingCategoryPair", field_name=field
        )
        for field, _ in profiling_category_pair_enum.fields
    )
    profiling_category_pair_enum.append_conversion_impl(
        RUST_MATCH_SELF.format(fields=profiling_category_pair_impl_fields)
    )
    profiling_category_enum.append_conversion_impl("      *self as u32")

    # After adding all the sub category enums, we can add the main enums to the list.
    enums.append(profiling_category_pair_enum)
    enums.append(profiling_category_enum)

    # Print all the enums and their impls.
    contents = "\n".join(map(lambda enum: enum.to_rust_string(), enums))
    generate_rust_file(c_out, contents)