Skip to content

Commit

Permalink
Merge pull request #737 from mdboom/python-timespan-metric-type
Browse files Browse the repository at this point in the history
Python: Implement timespan metric type
  • Loading branch information
badboy authored Feb 26, 2020
2 parents 9bb3ebd + 1d16cac commit c31a992
Show file tree
Hide file tree
Showing 9 changed files with 465 additions and 21 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[Full changelog](https://github.com/mozilla/glean/compare/v25.0.0...master)

* Python:
* The Boolean and Datetime metric types are now supported in Python.
* The Boolean, Datetime and Timespan metric types are now supported in Python.

# v25.0.0 (2020-02-17)

Expand Down
48 changes: 47 additions & 1 deletion docs/user/metrics/timespan.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ assertTrue(Auth.INSTANCE.loginTime.testHasValue())
assertTrue(Auth.INSTANCE.loginTime.testGetValue() > 0)
// Was the timing recorded incorrectly?
assertEquals(
1,
1,
Auth.INSTANCE.loginTime.testGetNumRecordedErrors(
ErrorType.InvalidValue
)
Expand Down Expand Up @@ -151,6 +151,40 @@ XCTAssertEqual(1, Auth.loginTime.testGetNumRecordedErrors(.invalidValue))

</div>

<div data-lang="Python" class="tab">

```Python
from glean import load_metrics
metrics = load_metrics("metrics.yaml")

def on_show_login():
metrics.auth.login_time.start()
# ...

def on_login():
metrics.auth.login_time.stop()
# ...

def on_login_cancel():
metrics.auth.login_time.cancel()
# ...
```

The time reported in the telemetry ping will be timespan recorded during the lifetime of the ping.

There are test APIs available too:

```Python
# Was anything recorded?
assert metrics.auth.login_time.test_has_value()
# Does the timer have the expected value
assert metrics.auth.login_time.test_get_value() > 0
# Was the timing recorded incorrectly?
assert 1 == metrics.auth.local_time.test_get_num_recorded_errors(
ErrorType.INVALID_VALUE
)
```

{{#include ../../tab_footer.md}}

## Raw API
Expand Down Expand Up @@ -185,6 +219,17 @@ HistorySync.setRawNanos(duration)

</div>

<div data-lang="Python" class="tab">

```Python
import org.mozilla.yourApplication.GleanMetrics.HistorySync

val duration = SyncResult.status.syncs.took.toLong()
HistorySync.setRawNanos(duration)
```

</div>

{{#include ../../tab_footer.md}}

## Limits
Expand All @@ -208,3 +253,4 @@ HistorySync.setRawNanos(duration)

* [Kotlin API docs](../../../javadoc/glean/mozilla.telemetry.glean.private/-timespan-metric-type/index.html)
* [Swift API docs](../../../swift/Classes/TimespanMetricType.html)
* [Python API docs](../../../python/glean/metrics/timespan.html)
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ class TimespanMetricTypeTest {
metric.setRawNanos(timespanNanos)
metric.stop()

assertNotEquals(timespanNanos, metric.testGetValue())
// If setRawNanos worked, (which it's not supposed to in this case), it would
// have recorded 1000000000 ns == 1s. Make sure it's not that.
assertNotEquals(1, metric.testGetValue())
}
}
1 change: 1 addition & 0 deletions glean-core/python/glean/_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"labeled_counter": metrics.LabeledCounterMetricType,
"ping": metrics.PingType,
"string": metrics.StringMetricType,
"timespan": metrics.TimespanMetricType,
"uuid": metrics.UuidMetricType,
}

Expand Down
2 changes: 2 additions & 0 deletions glean-core/python/glean/metrics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .lifetime import Lifetime
from .ping import PingType
from .string import StringMetricType
from .timespan import TimespanMetricType
from .timeunit import TimeUnit
from .uuid import UuidMetricType

Expand All @@ -27,6 +28,7 @@
"RecordedEventData",
"RecordedExperimentData",
"StringMetricType",
"TimespanMetricType",
"TimeUnit",
"UuidMetricType",
]
187 changes: 187 additions & 0 deletions glean-core/python/glean/metrics/timespan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# 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/.


from typing import List, Optional


from .. import _ffi
from .._dispatcher import Dispatcher
from ..testing import ErrorType
from .. import util


from .lifetime import Lifetime
from .timeunit import TimeUnit


class TimespanMetricType:
"""
This implements the developer facing API for recording timespan metrics.
Instances of this class type are automatically generated by
`glean.load_metrics`, allowing developers to record values that were
previously registered in the metrics.yaml file.
The timespan API exposes the `TimespanMetricType.start`,
`TimespanMetricType.stop` and `TimespanMetricType.cancel` methods.
"""

def __init__(
self,
disabled: bool,
category: str,
lifetime: Lifetime,
name: str,
send_in_pings: List[str],
time_unit: TimeUnit,
):
self._disabled = disabled
self._send_in_pings = send_in_pings

self._handle = _ffi.lib.glean_new_timespan_metric(
_ffi.ffi_encode_string(category),
_ffi.ffi_encode_string(name),
_ffi.ffi_encode_vec_string(send_in_pings),
len(send_in_pings),
lifetime.value,
disabled,
time_unit.value,
)

def __del__(self):
if getattr(self, "_handle", 0) != 0:
_ffi.lib.glean_destroy_timespan_metric(self._handle)

def start(self):
"""
Start tracking time for the provided metric.
This records an error if it’s already tracking time (i.e. `start` was
already called with no corresponding `stop`): in that case the original
start time will be preserved.
"""
if self._disabled:
return

start_time = util.time_ns()

@Dispatcher.launch
def start():
_ffi.lib.glean_timespan_set_start(self._handle, start_time)

def stop(self):
"""
Stop tracking time for the provided metric.
Sets the metric to the elapsed time, but does not overwrite an already
existing value.
This will record an error if no `start` was called or there is an already
existing value.
"""
if self._disabled:
return

stop_time = util.time_ns()

@Dispatcher.launch
def stop():
_ffi.lib.glean_timespan_set_stop(self._handle, stop_time)

def cancel(self):
"""
Abort a previous `start` call. No error is recorded if no `start` was called.
"""
if self._disabled:
return

@Dispatcher.launch
def cancel():
_ffi.lib.glean_timespan_cancel(self._handle)

def set_raw_nanos(self, elapsed_nanos: int):
"""
Explicitly set the timespan value, in nanoseconds.
This API should only be used if your library or application requires recording
times in a way that can not make use of [start]/[stop]/[cancel].
[setRawNanos] does not overwrite a running timer or an already existing value.
Args:
elapsed_nanos (int): The elapsed time to record, in nanoseconds.
"""
if self._disabled:
return

@Dispatcher.launch
def set_raw_nanos():
_ffi.lib.glean_timespan_set_raw_nanos(self._handle, elapsed_nanos)

def test_has_value(self, ping_name: Optional[str] = None) -> bool:
"""
Tests whether a value is stored for the metric for testing purposes
only.
Args:
ping_name (str): (default: first value in send_in_pings) The name
of the ping to retrieve the metric for.
Returns:
has_value (bool): True if the metric value exists.
"""
if ping_name is None:
ping_name = self._send_in_pings[0]

return bool(
_ffi.lib.glean_timespan_test_has_value(
self._handle, _ffi.ffi_encode_string(ping_name)
)
)

def test_get_value(self, ping_name: Optional[str] = None) -> int:
"""
Returns the stored value for testing purposes only.
Args:
ping_name (str): (default: first value in send_in_pings) The name
of the ping to retrieve the metric for.
Returns:
value (bool): value of the stored metric.
"""
if ping_name is None:
ping_name = self._send_in_pings[0]

if not self.test_has_value(ping_name):
raise ValueError("metric has no value")

return _ffi.lib.glean_timespan_test_get_value(
self._handle, _ffi.ffi_encode_string(ping_name)
)

def test_get_num_recorded_errors(
self, error_type: ErrorType, ping_name: Optional[str] = None
) -> int:
"""
Returns the number of errors recorded for the given metric.
Args:
error_type (ErrorType): The type of error recorded.
ping_name (str): (default: first value in send_in_pings) The name
of the ping to retrieve the metric for.
Returns:
num_errors (int): The number of errors recorded for the metric for
the given error type.
"""
if ping_name is None:
ping_name = self._send_in_pings[0]

return _ffi.lib.glean_timespan_test_get_num_recorded_errors(
self._handle, error_type.value, _ffi.ffi_encode_string(ping_name),
)


__all__ = ["TimespanMetricType"]
16 changes: 11 additions & 5 deletions glean-core/python/glean/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,27 @@ def get_locale_tag() -> str:
def time_ms() -> int:
"""
Get time from a monotonic timer in milliseconds.
On Python prior to 3.7, this may have less than millisecond resolution.
"""
return int(time.time_ns() / 1000)
return int(time.monotonic_ns() / 1000000.0)

time_ns = time.monotonic_ns


else:

def time_ms() -> int:
"""
Get time from a monotonic timer in milliseconds.
"""
return int(time.monotonic() * 1000.0)

def time_ns() -> int:
"""
Get time from a monotonic timer in nanoseconds.
On Python prior to 3.7, this may have less than millisecond resolution.
On Python prior to 3.7, this may have less than nanosecond resolution.
"""
return int(time.time() * 1000.0)
return int(time.monotonic() * 1000000000.0)


class classproperty:
Expand Down
Loading

0 comments on commit c31a992

Please sign in to comment.