Repository navigation
Add support to track the duration of fuzz task steps - #5555
brun0-matheus wants to merge 9 commits into
Conversation
ViniciustCosta
left a comment
There was a problem hiding this comment.
Let's do the bad_build_check metric in a separate PR and only add the time tracker structure and plumbing needed for the metrics in this one, so it's easier to review.
| pass | ||
|
|
||
|
|
||
| class TimeStatTracker: |
There was a problem hiding this comment.
This largely overlaps with TimeMetricTracker just above. Could we extend that class instead? Or maybe remove the old class as it is only used, if I remember correctly, by the corpus sync metric you added?
I'd rather not keep two trackers that differ only slightly.
There was a problem hiding this comment.
Yes, I suggest removing TimeMetricTracker in a future PR.
| testcase_execution_duration: datetime.timedelta = None, | ||
| fuzzing_duration: datetime.timedelta = None): | ||
| fuzzing_duration: datetime.timedelta = None, | ||
| fuzz_task_steps_duration=None): |
There was a problem hiding this comment.
nit: please add a type hint like the other params: fuzz_task_steps_duration: dict[str, datetime.timedelta] | None = None.
|
|
||
| self.gcs_corpus = None | ||
| self.fuzz_task_output = uworker_msg_pb2.FuzzTaskOutput() # pylint: disable=no-member | ||
| self.tracker = monitor.TimeStatTracker( |
There was a problem hiding this comment.
nit: self.tracker is too generic on FuzzingSession. Maybe self.steps_time_tracker?
| start = time.time() | ||
| success = True | ||
| try: | ||
| yield None |
There was a problem hiding this comment.
nit: yield None -> yield
| if fuzzing_duration is not None: | ||
| self._stats_data['fuzzing_duration'] = _timedelta_to_duration_string( | ||
| fuzzing_duration) | ||
| if fuzz_task_steps_duration: |
There was a problem hiding this comment.
If we keep the fixed RECORD, please drop and log keys that aren't in SCHEMA here for visibility, since these keys come from the uworker output.
| known_crash_count, groups, testcases_generated, | ||
| testcase_generation_duration, | ||
| testcase_execution_duration, fuzzing_duration) | ||
| job_run = fuzzer_stats.JobRun( |
There was a problem hiding this comment.
very nit: maybe in a follow-up PR we can try to use keyword args here to avoid ordering errors as this function now receives too many args.
| self.fuzz_task_output.fuzzing_duration.FromTimedelta( | ||
| datetime.timedelta(seconds=fuzzing_session_duration_seconds)) | ||
|
|
||
| for name, duration in self.tracker.stats.items(): |
There was a problem hiding this comment.
iiuc stats are only getting copied into fuzz_task_output at the very end of run, so any early return drops every step tracked so far. Not sure if there is any successful early returns, in which case we could write each step into as soon as it finishes.
FYI JobRun only reaches BQ for successfull sessions, while the Cloud Monitoring metric is emitted for every session that reaches a tracked step. So the two will have distinct values, e.g. failed or errored sessions only show up in Monitoring. Is that fine for how these step durations will be analysed? Maybe worth noting in a comment near the BQ field
There was a problem hiding this comment.
Yes, this is the expected behavior and there are no successful early returns. Actually, I'd prefer to have both of them (CloudMonitoring and BigQuery) track unsuccessful runs as well, but this is very hard to do with BQ because of how CF works, so I kept this implementation only for CM.
I agree to adding a comment in the BQ and CM metrics description to clarify this.
| except: | ||
| success = False | ||
| raise | ||
| finally: |
There was a problem hiding this comment.
Are there cases when the same step runs more than once (loops, retries)? Right now it warns and overwrites. Should durations be summed instead?
There was a problem hiding this comment.
Yes, there are. In this case I opted to make a tracker for each loop (as in PR #5569). Maybe summing it up is a simpler solution.
As a matter of fact, I opted for the first solution (make a tracker for each loop) in order to get the stats for TestcaseRun, but later on (PR #5571) I had to do the same for blackbox fuzzing, so now I don't think it's the best solution anymore.
| self.stats: dict[str, datetime.timedelta] = {} | ||
| self.metric = metric | ||
| self.common_labels = dict(common_labels) if common_labels else {} | ||
| if metric: |
There was a problem hiding this comment.
This checks a hard-coded metric at runtime on every FuzzingSession. Isn't a unit test enough?
There was a problem hiding this comment.
I don't get it. Are you referring to the if metric part? I wanted to make this generic and work for any metric, so it's easy to extend to other code parts.
| // Total fuzzing session time. Includes time spent updating the fuzzer, build | ||
| // and test corpus set up, and crash processing. | ||
| optional google.protobuf.Duration fuzzing_duration = 23; | ||
| map<string, google.protobuf.Duration> fuzz_task_steps_duration = 24; |
There was a problem hiding this comment.
Please add a comment like the above field have.
Also nit: I think steps_duration is enough inside FuzzTaskOutput
| 'type': 'INTERVAL', | ||
| 'mode': 'NULLABLE' | ||
| }, { | ||
| 'name': |
There was a problem hiding this comment.
Out of curiosity, did you look into possibly adding another table instead of adding a struct column?
There was a problem hiding this comment.
No. I believe this place is good enough, since it's basically extra stats to the fuzz task run.
| # TODO(metzman): Replace fuzz_task_output.fully_qualified_fuzzer_name` with | ||
| # `fuzz_task_input.fuzz_target` instead. |
There was a problem hiding this comment.
It seems that this comment should be after the code you added.
Done, now the bad build check PR is #5574. |
Bug: b/577973994
Overview
Instruments the code to be able to easily emit metrics related to the duration of each fuzz_task step. The metrics are emitted both to Cloud Monitoring (using only a combination of fuzzer name and job, to keep the cardinality low) and Big Query.
Changes
TimeStatTracker, which is a reusable context manager that measures the time spent in its code context, saving it to a local dictionary (used to send the measurements toFuzzTaskOutputprotobuf later on) and emitting it to a supplied Cloud Monitoring metric.fuzz_task_steps_durationtoFuzzTaskOutputprotobuf, which is a map of string to interval duration, which allows to easily extend the code to support additional steps.has_fieldmethod in Metric, in order to make sure the metric supplied toTimeStatTrackerhas the correct fields.FUZZ_TASK_STEPS_DURATIONmetric in Cloud Monitoring. It contains the same labels asFUZZING_SESSION_DURATION, with the addition of thestepandsuccesslabels.fuzz_task_steps_durationfield in the JobRun schema (BigQuery), which contains the duration of all steps. Currently it only contains thebad_build_checkduration.load_bigquery_statstest. Now it uses the schema defined infuzzer_stats.py, avoiding duplicated code.Tests
Additional unit tests created. I ran tests in the dev environment with the bad_build check implemented.
Evidence that the CloudMonitoring metric is being tracked: https://screenshot.corp.google.com/2u8b1d3psrv68.
Evidence that the BigQuery metric is being tracked: https://screenshot-v2.corp.google.com/73759kq0eje1o.