Skip to content
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

Python: Implement timespan metric type #737

Merged
merged 3 commits into from
Feb 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
badboy marked this conversation as resolved.
Show resolved Hide resolved
}
}
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