Skip to content

Commit

Permalink
[sai-gen] Add support for generating SAI stats APIs for counters. (#514)
Browse files Browse the repository at this point in the history
## Problem

DASH has scaling requirement that requires technology providers to support huge number of objects, e.g. 8M of CA-PA mappings. If we need to add counters for these objects by using generic SAI counters, we will generate large number of counter ids, which might not be ideal.

Also some counters we are collecting are not data path counters, but more like hit counts, which is not what generic SAI counter is modeling.

For these cases, we can support generating the counters as SAI stats and leverage existing SAI stats API design for querying the counters back.

## What we are doing in this change

This change updated sai_api_gen.py with 2 changes:
1. Rename `as_attr` to `attr_type` for supporting more types of counter generation.
2. Updated the counter generation logic and its templates for generating SAI stats APIs.

This feature will be used in the incoming fast path API generations.
  • Loading branch information
r12f authored Feb 13, 2024
1 parent 8ba8678 commit 1d5f8ce
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 33 deletions.
83 changes: 56 additions & 27 deletions dash-pipeline/SAI/sai_api_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ def __init__(self):
self.bitwidth: int = 64
self.isreadonly: str = "true"
self.counter_type: str = "bytes"
self.as_attr: bool = False
self.attr_type: str = "stats"
self.param_actions: List[str] = []

def parse_p4rt(self, p4rt_counter: Dict[str, Any], var_ref_graph: P4VarRefGraph) -> None:
Expand All @@ -539,9 +539,8 @@ def parse_p4rt(self, p4rt_counter: Dict[str, Any], var_ref_graph: P4VarRefGraph)
print("Parsing counter: " + self.name)
self.__parse_sai_counter_annotation(p4rt_counter)

# If this counter is marked as SAI attributes, then we need to generate dedicated SAI attributes for this counter.
# In this case, the type needs to be created based on the size.
if self.as_attr:
# If this counter needs to be generated as SAI attributes, we need to figure out the data type for the counter value.
if self.attr_type != "counter_id":
counter_storage_type = SAITypeSolver.get_object_sai_type(self.bitwidth)

# Otherwise, this counter should be linked to a SAI counter using an object ID.
Expand Down Expand Up @@ -594,28 +593,32 @@ def __parse_sai_counter_annotation(self, p4rt_counter: Dict[str, Any]) -> None:
continue
elif kv['key'] == 'action_names':
self.param_actions = str(kv['value']['stringValue']).split(",")
elif kv['key'] == 'as_attr':
self.as_attr = True if kv['value']['stringValue'] == "true" else False
elif kv['key'] == 'attr_type':
self.attr_type = str(kv['value']['stringValue'])
if self.attr_type not in ["counter_attr", "counter_id", "stats"]:
raise ValueError(f'Unknown counter attribute type: attr_type={self.attr_type}')
else:
raise ValueError("Unknown attr annotation " + kv['key'])

def generate_final_counter_on_type(self) -> 'Iterator[SAICounter]':
# If this counter is not used as an attribute, then we need to treat it as a counter object id.
if not self.as_attr:
def generate_counter_sai_attributes(self) -> 'Iterator[SAICounter]':
# If the SAI attribute type is counter id, we generate as standard SAI counter ID attributes, hence return as it is.
if self.attr_type == "counter_id":
yield self

# Otherwise, we need to generate dedicated SAI attributes for this counter.
else:
if self.counter_type != 'both':
self.name = f"{self.name}_{self.counter_type}_counter"
yield self
counter_types = ['bytes', 'packets'] if self.counter_type == 'both' else [self.counter_type]

for index, counter_type in enumerate(counter_types):
counter = self
if index != len(counter_types) - 1:
counter = copy.deepcopy(self)

counter.counter_type = counter_type
if counter.attr_type == "counter_attr":
counter.name = f"{counter.name}_{counter.counter_type}_counter"
else:
packets_counter = copy.deepcopy(self)
packets_counter.name = f"{packets_counter.name}_packets_counter"
yield packets_counter
counter.name = f"{counter.name}_{counter.counter_type}"

self.name = f"{self.name}_bytes_counter"
yield self
yield counter


@sai_parser_from_p4rt
Expand Down Expand Up @@ -764,6 +767,7 @@ def __init__(self):
self.counters: List[SAICounter] = []
self.with_counters: str = 'false'
self.sai_attributes: List[SAIAPITableAttribute] = []
self.sai_stats: List[SAIAPITableAttribute] = []

# Extra properties from annotations
self.stage: Optional[str] = None
Expand Down Expand Up @@ -940,21 +944,31 @@ def __update_table_param_object_name_reference(self, all_table_names) -> None:
key.object_name = table_name

def __build_sai_attributes_after_parsing(self):
# Group all actions parameters and counters with as_attr set by order with sequence kept the same.
# Group all actions parameters and counters set by order with sequence kept the same.
# Then merge them into a single list.
sai_attributes_by_order = {}
sai_stats_by_order = {}

for action_param in self.action_params:
if action_param.skipattr != "true":
sai_attributes_by_order.setdefault(action_param.order, []).append(action_param)

for counter in self.counters:
sai_attributes_by_order.setdefault(counter.order, []).append(counter)
if counter.attr_type != "stats":
sai_attributes_by_order.setdefault(counter.order, []).append(counter)
else:
sai_stats_by_order.setdefault(counter.order, []).append(counter)

# Merge all attributes into a single list.
# Merge all attributes into a single list by their order.
self.sai_attributes = []
for order in sorted(sai_attributes_by_order.keys()):
self.sai_attributes.extend(sai_attributes_by_order[order])

# Merge all stat counters into a single list by their order.
self.sai_stats = []
for order in sorted(sai_stats_by_order.keys()):
self.sai_stats.extend(sai_stats_by_order[order])


class DASHAPISet(SAIObject):
'''
Expand Down Expand Up @@ -1010,7 +1024,7 @@ def __parse_sai_counters_from_p4rt(self, p4rt_value: Dict[str, Any], var_ref_gra
all_p4rt_counters = p4rt_value[COUNTERS_TAG]
for p4rt_counter in all_p4rt_counters:
counter = SAICounter.from_p4rt(p4rt_counter, var_ref_graph)
self.sai_counters.extend(counter.generate_final_counter_on_type())
self.sai_counters.extend(counter.generate_counter_sai_attributes())

def __parse_sai_apis_from_p4rt(self, program: Dict[str, Any], ignore_tables: List[str]) -> None:
# Group all counters by action name.
Expand Down Expand Up @@ -1219,25 +1233,40 @@ def generate_sai_port_extensions(self) -> None:

# If any counter doesn't have any table assigned, they should be added as port attributes and track globally.
new_port_counters: List[SAICounter] = []
new_port_stats: List[SAICounter] = []
is_first_attr = False
is_first_stat = False
with open('SAI/experimental/saiportextensions.h', 'r') as f:
content = f.read()

all_port_attrs = re.findall(r'SAI_PORT_ATTR_\w+', content)
is_first_attr = len(all_port_attrs) == 3

all_port_stats = re.findall(r'SAI_PORT_STAT_\w+', content)
is_first_stat = len(all_port_stats) == 3

for sai_counter in self.dash_sai_ext.sai_counters:
if len(sai_counter.param_actions) == 0 and sai_counter.as_attr == False:
sai_counter_port_attr_name = f"SAI_PORT_ATTR_{sai_counter.name.upper()}"
if sai_counter_port_attr_name not in all_port_attrs:
new_port_counters.append(sai_counter)
if len(sai_counter.param_actions) == 0:
if sai_counter.attr_type != "stats":
sai_counter_port_attr_name = f"SAI_PORT_ATTR_{sai_counter.name.upper()}"
if sai_counter_port_attr_name not in all_port_attrs:
new_port_counters.append(sai_counter)
else:
sai_counter_port_stat_name = f"SAI_PORT_STAT_{sai_counter.name.upper()}"
if sai_counter_port_stat_name not in all_port_stats:
new_port_stats.append(sai_counter)

sai_counters_str = SAITemplateRender('templates/saicounter.j2').render(table_name = "port", sai_counters = new_port_counters, is_first_attr = is_first_attr)
sai_counters_lines = [s.rstrip(" \n") for s in sai_counters_str.split('\n')]
sai_counters_lines = sai_counters_lines[:-1] # Remove the last empty line, so we won't add extra empty line to the file.

sai_stats_str = SAITemplateRender('templates/headers/sai_stats_extensions.j2').render(table_name = "port", sai_stats = new_port_stats, is_first_attr = is_first_stat)
sai_stats_lines = [s.rstrip(" \n") for s in sai_stats_str.split('\n')]
sai_stats_lines = sai_stats_lines[:-1] # Remove the last empty line, so we won't add extra empty line to the file.

with SAIFileUpdater('SAI/experimental/saiportextensions.h') as f:
f.insert_before('Add new experimental port attributes above this line', sai_counters_lines)
f.insert_before('Add new experimental port stats above this line', sai_stats_lines)

def generate_sai_object_extensions(self) -> None:
print("\nGenerating SAI object entry extensions ...")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if table.is_object == 'true' %}
* @param[in] {{ table.name }}_id Entry id
{% else %}
* @param[in] {{ table.name }} Entry
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if table.is_object == 'true' %}
_In_ sai_object_id_t {{ table.name }}_id,
{% else %}
_In_ const sai_{{ table.name }}_t *{{ table.name }},
{% endif %}
50 changes: 50 additions & 0 deletions dash-pipeline/SAI/templates/headers/sai_stats_api.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{% if table.sai_stats | length > 0 %}
/**
* @brief Get {{ table.name }} statistics counters. Deprecated for backward compatibility.
*
{% include 'templates/headers/sai_api_comment_object_id.j2' %}
* @param[in] number_of_counters Number of counters in the array
* @param[in] counter_ids Specifies the array of counter ids
* @param[out] counters Array of resulting counter values.
*
* @return #SAI_STATUS_SUCCESS on success, failure status code on error
*/
typedef sai_status_t (*sai_get_{{ table.name }}_stats_fn)(
{% include 'templates/headers/sai_api_param_object_id.j2' %}
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_Out_ uint64_t *counters);

/**
* @brief Get {{ table.name }} statistics counters extended.
*
{% include 'templates/headers/sai_api_comment_object_id.j2' %}
* @param[in] number_of_counters Number of counters in the array
* @param[in] counter_ids Specifies the array of counter ids
* @param[in] mode Statistics mode
* @param[out] counters Array of resulting counter values.
*
* @return #SAI_STATUS_SUCCESS on success, failure status code on error
*/
typedef sai_status_t (*sai_get_{{ table.name }}_stats_ext_fn)(
{% include 'templates/headers/sai_api_param_object_id.j2' %}
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_In_ sai_stats_mode_t mode,
_Out_ uint64_t *counters);

/**
* @brief Clear {{ table.name }} statistics counters.
*
{% include 'templates/headers/sai_api_comment_object_id.j2' %}
* @param[in] number_of_counters Number of counters in the array
* @param[in] counter_ids Specifies the array of counter ids
*
* @return #SAI_STATUS_SUCCESS on success, failure status code on error
*/
typedef sai_status_t (*sai_clear_{{ table.name }}_stats_fn)(
{% include 'templates/headers/sai_api_param_object_id.j2' %}
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids);

{% endif %}
5 changes: 5 additions & 0 deletions dash-pipeline/SAI/templates/headers/sai_stats_api_list.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if table.sai_stats | length > 0 %}
sai_get_{{ table.name }}_stats_fn {{ " " * space_offset }}get_{{ table.name }}_stats;
sai_get_{{ table.name }}_stats_ext_fn {{ " " * space_offset }}get_{{ table.name }}_stats_ext;
sai_clear_{{ table.name }}_stats_fn {{ " " * space_offset }}clear_{{ table.name }}_stats;
{% endif %}
14 changes: 14 additions & 0 deletions dash-pipeline/SAI/templates/headers/sai_stats_enum.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% if table.sai_stats | length > 0 %}
/**
* @brief Counter IDs for {{ table.name }} in sai_get_{{ table.name }}_stats() call
*/
typedef enum _sai_{{ table.name }}_stat_t
{
{% for stat in table.sai_stats %}
/** DASH {{ table.name | lower }} {{ stat.name | upper }} stat count */
SAI_{{ table.name | upper }}_STAT_{{ stat.name | upper }},

{% endfor %}
} sai_{{ table.name }}_stat_t;

{% endif %}
11 changes: 11 additions & 0 deletions dash-pipeline/SAI/templates/headers/sai_stats_extensions.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% set sai_stats_extensions_ns = namespace(is_first_attr = is_first_attr) %}
{% for stat in sai_stats %}
/** DASH {{ table_name | lower }} {{ stat.name | upper }} stat count */
{% if sai_stats_extensions_ns.is_first_attr == true %}
SAI_{{ table_name | upper }}_STAT_{{ stat.name | upper }} = SAI_{{ table_name | upper }}_STAT_EXTENSIONS_RANGE_START,
{% else %}
SAI_{{ table_name | upper }}_STAT_{{ stat.name | upper }},
{% endif %}
{% set sai_stats_extensions_ns.is_first_attr = false %}

{% endfor %}
74 changes: 74 additions & 0 deletions dash-pipeline/SAI/templates/saiapi.cpp.j2
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,41 @@ static sai_status_t dash_sai_get_{{ table.name }}_attribute(
assert(0 && "sai_get_{{ table.name }}_attribute NYI");
return SAI_STATUS_FAILURE;
}

{% if table.sai_stats | length > 0 %}
static sai_status_t dash_sai_get_{{ table.name }}_stats(
_In_ sai_object_id_t {{ table.name }}_id,
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_Out_ uint64_t *counters)
{
DASH_LOG_ENTER();
assert(0 && "sai_get_{{ table.name }}_stats NYI");
return SAI_STATUS_FAILURE;
}

static sai_status_t dash_sai_get_{{ table.name }}_stats_ext(
_In_ sai_object_id_t {{ table.name }}_id,
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_In_ sai_stats_mode_t mode,
_Out_ uint64_t *counters)
{
DASH_LOG_ENTER();
assert(0 && "sai_get_{{ table.name }}_stats_ext NYI");
return SAI_STATUS_FAILURE;
}

static sai_status_t dash_sai_clear_{{ table.name }}_stats(
_In_ sai_object_id_t {{ table.name }}_id,
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids)
{
DASH_LOG_ENTER();
assert(0 && "sai_clear_{{ table.name }}_stats NYI");
return SAI_STATUS_FAILURE;
}
{% endif %}
{% else %}
static sai_status_t dash_sai_create_{{ table.name }}(
_In_ const sai_{{ table.name }}_t *{{ table.name }},
Expand Down Expand Up @@ -625,6 +660,40 @@ static sai_status_t dash_sai_get_{{ table.name }}_attribute(
return SAI_STATUS_FAILURE;
}

{% if table.sai_stats | length > 0 %}
static sai_status_t dash_sai_get_{{ table.name }}_stats(
_In_ const sai_{{ table.name }}_t *{{ table.name }},
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_Out_ uint64_t *counters)
{
DASH_LOG_ENTER();
assert(0 && "sai_get_{{ table.name }}_stats NYI");
return SAI_STATUS_FAILURE;
}

static sai_status_t dash_sai_get_{{ table.name }}_stats_ext(
_In_ const sai_{{ table.name }}_t *{{ table.name }},
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids,
_In_ sai_stats_mode_t mode,
_Out_ uint64_t *counters)
{
DASH_LOG_ENTER();
assert(0 && "sai_get_{{ table.name }}_stats_ext NYI");
return SAI_STATUS_FAILURE;
}

static sai_status_t dash_sai_clear_{{ table.name }}_stats(
_In_ const sai_{{ table.name }}_t *{{ table.name }},
_In_ uint32_t number_of_counters,
_In_ const sai_stat_id_t *counter_ids)
{
DASH_LOG_ENTER();
assert(0 && "sai_clear_{{ table.name }}_stats NYI");
return SAI_STATUS_FAILURE;
}
{% endif %}
{% if table.name == 'route_entry' %}

static sai_status_t dash_sai_set_{{ table.name | replace("entry", "entries") }}_attribute(
Expand Down Expand Up @@ -666,6 +735,11 @@ sai_{{ app_name }}_api_t dash_sai_{{ app_name }}_api_impl = {
.remove_{{ table.name }} = dash_sai_remove_{{ table.name }},
.set_{{ table.name }}_attribute = dash_sai_set_{{ table.name }}_attribute,
.get_{{ table.name }}_attribute = dash_sai_get_{{ table.name }}_attribute,
{% if table.sai_stats | length > 0 %}
.get_{{ table.name }}_stats = dash_sai_get_{{ table.name }}_stats,
.get_{{ table.name }}_stats_ext = dash_sai_get_{{ table.name }}_stats_ext,
.clear_{{ table.name }}_stats = dash_sai_clear_{{ table.name }}_stats,
{% endif %}
{% if table.is_object == 'true' %}
.create_{{ table.name }}s = dash_sai_create_{{ table.name }}s,
.remove_{{ table.name }}s = dash_sai_remove_{{ table.name }}s,
Expand Down
3 changes: 3 additions & 0 deletions dash-pipeline/SAI/templates/saiapi.h.j2
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ typedef enum _sai_{{ table.name }}_attr_t

} sai_{{ table.name }}_attr_t;

{% include 'templates/headers/sai_stats_enum.j2' %}
{% endfor %}
{% for table in sai_api.tables %}
/**
Expand Down Expand Up @@ -335,6 +336,7 @@ typedef sai_status_t (*sai_get_{{ table.name }}_attribute_fn)(
_In_ uint32_t attr_count,
_Inout_ sai_attribute_t *attr_list);

{% include 'templates/headers/sai_stats_api.j2' %}
{% if table.is_object == 'false' %}
/**
* @brief Bulk create {{ sai_api.app_name }}_{{ table.name }}
Expand Down Expand Up @@ -392,6 +394,7 @@ typedef struct _sai_{{ sai_api.app_name }}_api_t
sai_remove_{{ table.name }}_fn {{ " " * space_offset }}remove_{{ table.name }};
sai_set_{{ table.name }}_attribute_fn {{ " " * space_offset }}set_{{ table.name }}_attribute;
sai_get_{{ table.name }}_attribute_fn {{ " " * space_offset }}get_{{ table.name }}_attribute;
{% include 'templates/headers/sai_stats_api_list.j2' %}
{% if table.is_object == 'true' %}
sai_bulk_object_create_fn{{ " " * table_name_max }}create_{{ table.name }}s;
sai_bulk_object_remove_fn{{ " " * table_name_max }}remove_{{ table.name }}s;
Expand Down
Loading

0 comments on commit 1d5f8ce

Please sign in to comment.