-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
instrument NADE commands using metric spans #1844
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
file renamed from test_feature_metrics
since it was going to be too large with the spans, and also converted the tests to use the factories
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally the test directory mirrors the layout of the source directory so that tests are easy to locate. Breaking down the files like this because of some arbitrary size constraint makes things easier to manage, and makes it easier for us to review the changes because we can no longer see the diff. Please follow the convention instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't follow this comment, did you mean to say it makes it harder if we split the test file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, I merged the new integ tests and also the old unit tests to test_metrics.py
in their respective folders, and also adjusted the codeowners file accordingly, let me know if this works for you!
[ | ||
( | ||
"app deploy", | ||
lambda: ProjectV11Factory( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the best I could come up with to have parametrization with factories, if anyone has a better approach I'd be interested
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do we need parametrized tests at all? I don't think I can read this anymore. I think overall we make way, way too frequent use of parametrized tests in our codebase, but that's a different story.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the arguments to the factories are the same, you can parametrize just on the class and instantiate it in the test
@pytest.mark.parametrize("factory_class", [FooFactory, BarFactory])
def test_factory(factory_class):
obj = factory_class(arg=123)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up getting rid of all the parametrization and separating the tests out since the factories didn't have the same arguments, I think it's more clear and separated this way, let me know what you think
description + pre-review checklist aren't optional, please fill them in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally the test directory mirrors the layout of the source directory so that tests are easy to locate. Breaking down the files like this because of some arbitrary size constraint makes things easier to manage, and makes it easier for us to review the changes because we can no longer see the diff. Please follow the convention instead.
@@ -88,7 +88,7 @@ def compile_artifacts(self): | |||
if not self._should_invoke_processors(): | |||
return | |||
|
|||
with cc.phase("Invoking artifact processors"): | |||
with cc.phase("Invoking artifact processors", span_name="compile_artifacts"): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's no compilation happening. How about simply "artifact_processors"? We should probably also have individual processors as sub-spans.
@@ -213,6 +214,26 @@ def get_cli_context() -> _CliGlobalContextAccess: | |||
return _CliGlobalContextAccess(get_cli_context_manager()) | |||
|
|||
|
|||
def start_cli_metrics_span(span_name: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think "start" makes sense here, since this both starts and stops the span at the appropriate time. Let's workshop the name of this + the start_span
method it calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, I feel @span would suffice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed both the method name and the decorator name to just span
, let me know if this works for you
@@ -420,6 +421,7 @@ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]: | |||
).fetchall() | |||
return [{"name": row[1], "type": row[2]} for row in results] | |||
|
|||
@start_cli_metrics_span("create_or_upgrade_app") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spans shouldn't be implementation-specific, but rather represent a logic step in the overall operation being performed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjusted the names and locations of all the spans to better encapsulate this, let me know if they work for you!
@@ -915,6 +916,7 @@ def validate_setup_script( | |||
if validation_result["status"] == "FAIL": | |||
raise SetupScriptFailedValidation() | |||
|
|||
@start_cli_metrics_span("get_validation_result") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto, let's not name things according to the implementation. That can change over time.
src/snowflake/cli/api/console/abc.py
Outdated
@@ -68,6 +68,7 @@ def phase( | |||
self, | |||
enter_message: str, | |||
exit_message: Optional[str] = None, | |||
span_name: Optional[str] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if this is truly needed, since we can use multiple context managers on the same line. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot about that feature, I adjusted these instances, thanks for the reminder
@@ -304,6 +308,7 @@ def validation_item_to_str(item: dict[str, str | int]): | |||
return s | |||
|
|||
|
|||
@start_cli_metrics_span("drop_generic_object") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
[ | ||
( | ||
"app deploy", | ||
lambda: ProjectV11Factory( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do we need parametrized tests at all? I don't think I can read this anymore. I think overall we make way, way too frequent use of parametrized tests in our codebase, but that's a different story.
… un-parametrize tests, merge test files to keep with covention
My mistake, I usually do it at the same time as when I make the PR but I started it as a draft this time and forgot to do it when I set it as ready for review, should be populated now! |
init=False, default_factory=lambda: time.monotonic() | ||
) | ||
# start time of the step from a performance counter in order to calculate execution time | ||
_start_time: float = field(init=False, default_factory=time.perf_counter) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ran into failing integ tests where the ordering of the spans was not deterministic, did some more research and found that time.perf_counter
should just be a better version of time.monotonic
with good granularity on Windows, so replaced those calls here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get that part - Why is time determining the order? The most accurate way is first call goes first. Or are we dealing with multithreads here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Windows, time.monotonic()
does not have as much precision, so multiple spans would end up having the same start time, so when we sort them by start time (to make it so that the first call goes first) the order ends up not being the same as the order as when we start the spans. This is fixed with time.perf_counter()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weird that the sorting function wasn't stable though. IMHO regardless of this "fix", we shouldn't rely on the uniqueness of timestamps and switch to a stable sort regardless.
def mock_time_monotonic(): | ||
with mock.patch("time.monotonic", side_effect=count()) as mocked_time_monotonic: | ||
yield mocked_time_monotonic | ||
def test_metrics_no_counters(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All counter tests moved from test_metrics_counters.py
to consolidate the files and keep to the convention
|
||
@pytest.mark.integration | ||
@mock.patch("snowflake.connector.telemetry.TelemetryClient.try_add_log_to_batch") | ||
def test_feature_counters_v2_post_deploy_not_available_in_bundle( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
counter tests moved from test_feature_metrics.py
to consolidate them in one file and keep to the convention, and converted to use factories
src/snowflake/cli/_plugins/nativeapp/entities/application_package.py
Outdated
Show resolved
Hide resolved
@@ -649,6 +650,7 @@ def resolve_without_follow(path: Path) -> Path: | |||
return Path(os.path.abspath(path)) | |||
|
|||
|
|||
@span("build_initial_bundle") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just bundle
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my head bundle
is the higher level container that does both this step (populating the deploy root) and then applying all the processors on it, but if we're fine just having the first step be called bundle then that's good with me
@@ -355,6 +355,7 @@ def post_deploy_hooks(self) -> list[PostDeployHook] | None: | |||
model = self._entity_model | |||
return model.meta and model.meta.post_deploy | |||
|
|||
@span("deploy_app") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should consider some kind of standardized scheme for actions + entities, maybe something like action.app.deploy
?
src/snowflake/cli/_plugins/nativeapp/entities/application_package.py
Outdated
Show resolved
Hide resolved
@@ -544,6 +546,7 @@ def _bundle(self): | |||
compiler.compile_artifacts() | |||
return bundle_map | |||
|
|||
@span("deploy_app_package") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would rather do this on the action for consistency
@@ -68,7 +68,11 @@ def _format_message(self, message: str, output: Output) -> Text: | |||
return text | |||
|
|||
@contextmanager | |||
def phase(self, enter_message: str, exit_message: Optional[str] = None): | |||
def phase( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're no longer touching this, I'd revert the file entirely
@@ -76,6 +76,7 @@ def _get_stage_paths_to_sync( | |||
return stage_paths | |||
|
|||
|
|||
@span("sync_remote_and_local_files") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is one of the rare cases where I'd use the actual function name, since it's exactly the logical concept we're timing.
Pre-review checklist
Changes description
An example trace of
snow app run
looks like the following (truncated for brevity):