| Index: third_party/google-endpoints/google/api/control/distribution.py
|
| diff --git a/third_party/google-endpoints/google/api/control/distribution.py b/third_party/google-endpoints/google/api/control/distribution.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..058632e0552decb357a967751321b4da01a67cef
|
| --- /dev/null
|
| +++ b/third_party/google-endpoints/google/api/control/distribution.py
|
| @@ -0,0 +1,381 @@
|
| +# Copyright 2016 Google Inc. All Rights Reserved.
|
| +#
|
| +# Licensed under the Apache License, Version 2.0 (the "License");
|
| +# you may not use this file except in compliance with the License.
|
| +# You may obtain a copy of the License at
|
| +#
|
| +# http://www.apache.org/licenses/LICENSE-2.0
|
| +#
|
| +# Unless required by applicable law or agreed to in writing, software
|
| +# distributed under the License is distributed on an "AS IS" BASIS,
|
| +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| +# See the License for the specific language governing permissions and
|
| +# limitations under the License.
|
| +
|
| +"""distribution provides funcs for working with `Distribution` instances.
|
| +
|
| +:func:`create_exponential`, :func:`create_linear`, :func:`create_linear`
|
| +construct new `Distribution` instances initialized with different types
|
| +of buckets a `Distribution` can have. They are factory functions that
|
| +include assertions that make sure that the Distribution instances are
|
| +in the correct state.
|
| +
|
| +:func:`add_sample` adds a sample to an existing distribution instance
|
| +
|
| +:func:`merge` merges two distribution instances
|
| +
|
| +"""
|
| +
|
| +from __future__ import absolute_import
|
| +from __future__ import division
|
| +
|
| +import bisect
|
| +import logging
|
| +import math
|
| +
|
| +from . import messages
|
| +
|
| +logger = logging.getLogger(__name__)
|
| +
|
| +
|
| +_BAD_NUM_FINITE_BUCKETS = 'number of finite buckets should be > 0'
|
| +_BAD_FLOAT_ARG = '%s should be > %f'
|
| +
|
| +
|
| +def create_exponential(num_finite_buckets, growth_factor, scale):
|
| + """Creates a new instance of distribution with exponential buckets
|
| +
|
| + Args:
|
| + num_finite_buckets (int): initializes number of finite buckets
|
| + growth_factor (float): initializes the growth factor
|
| + scale (float): initializes the scale
|
| +
|
| + Return:
|
| + :class:`google.api.gen.servicecontrol_v1_messages.Distribution`
|
| +
|
| + Raises:
|
| + ValueError: if the args are invalid for creating an instance
|
| + """
|
| + if num_finite_buckets <= 0:
|
| + raise ValueError(_BAD_NUM_FINITE_BUCKETS)
|
| + if growth_factor <= 1.0:
|
| + raise ValueError(_BAD_FLOAT_ARG % ('growth factor', 1.0))
|
| + if scale <= 0.0:
|
| + raise ValueError(_BAD_FLOAT_ARG % ('scale', 0.0))
|
| + return messages.Distribution(
|
| + bucketCounts=[0] * (num_finite_buckets + 2),
|
| + exponentialBuckets=messages.ExponentialBuckets(
|
| + numFiniteBuckets=num_finite_buckets,
|
| + growthFactor=growth_factor,
|
| + scale=scale))
|
| +
|
| +
|
| +def create_linear(num_finite_buckets, width, offset):
|
| + """Creates a new instance of distribution with linear buckets.
|
| +
|
| + Args:
|
| + num_finite_buckets (int): initializes number of finite buckets
|
| + width (float): initializes the width of each bucket
|
| + offset (float): initializes the offset
|
| +
|
| + Return:
|
| + :class:`google.api.gen.servicecontrol_v1_messages.Distribution`
|
| +
|
| + Raises:
|
| + ValueError: if the args are invalid for creating an instance
|
| + """
|
| + if num_finite_buckets <= 0:
|
| + raise ValueError(_BAD_NUM_FINITE_BUCKETS)
|
| + if width <= 0.0:
|
| + raise ValueError(_BAD_FLOAT_ARG % ('width', 0.0))
|
| + return messages.Distribution(
|
| + bucketCounts=[0] * (num_finite_buckets + 2),
|
| + linearBuckets=messages.LinearBuckets(
|
| + numFiniteBuckets=num_finite_buckets,
|
| + width=width,
|
| + offset=offset))
|
| +
|
| +
|
| +def create_explicit(bounds):
|
| + """Creates a new instance of distribution with explicit buckets.
|
| +
|
| + bounds is an iterable of ordered floats that define the explicit buckets
|
| +
|
| + Args:
|
| + bounds (iterable[float]): initializes the bounds
|
| +
|
| + Return:
|
| + :class:`google.api.gen.servicecontrol_v1_messages.Distribution`
|
| +
|
| + Raises:
|
| + ValueError: if the args are invalid for creating an instance
|
| + """
|
| + safe_bounds = sorted(float(x) for x in bounds)
|
| + if len(safe_bounds) != len(set(safe_bounds)):
|
| + raise ValueError('Detected two elements of bounds that are the same')
|
| + return messages.Distribution(
|
| + bucketCounts=[0] * (len(safe_bounds) + 1),
|
| + explicitBuckets=messages.ExplicitBuckets(bounds=safe_bounds))
|
| +
|
| +
|
| +def add_sample(a_float, dist):
|
| + """Adds `a_float` to `dist`, updating its existing buckets.
|
| +
|
| + Args:
|
| + a_float (float): a new value
|
| + dist (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + the Distribution being updated
|
| +
|
| + Raises:
|
| + ValueError: if `dist` does not have known bucket options defined
|
| + ValueError: if there are not enough bucket count fields in `dist`
|
| + """
|
| + dist_type, _ = _detect_bucket_option(dist)
|
| + if dist_type == 'exponentialBuckets':
|
| + _update_general_statistics(a_float, dist)
|
| + _update_exponential_bucket_count(a_float, dist)
|
| + elif dist_type == 'linearBuckets':
|
| + _update_general_statistics(a_float, dist)
|
| + _update_linear_bucket_count(a_float, dist)
|
| + elif dist_type == 'explicitBuckets':
|
| + _update_general_statistics(a_float, dist)
|
| + _update_explicit_bucket_count(a_float, dist)
|
| + else:
|
| + logger.error('Could not determine bucket option type for %s', dist)
|
| + raise ValueError('Unknown bucket option type')
|
| +
|
| +
|
| +def merge(prior, latest):
|
| + """Merge `prior` into `latest`.
|
| +
|
| + N.B, this mutates latest. It ensures that the statistics and histogram are
|
| + updated to correctly include the original values from both instances.
|
| +
|
| + Args:
|
| + prior (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + an instance
|
| + latest (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + an instance to be updated
|
| +
|
| + Raises:
|
| + ValueError: if the bucket options of `prior` and `latest` do not match
|
| + ValueError: if the bucket counts of `prior` and `latest` do not match
|
| +
|
| + """
|
| + if not _buckets_nearly_equal(prior, latest):
|
| + logger.error('Bucket options do not match. From %s To: %s',
|
| + prior,
|
| + latest)
|
| + raise ValueError('Bucket options do not match')
|
| + if len(prior.bucketCounts) != len(latest.bucketCounts):
|
| + logger.error('Bucket count sizes do not match. From %s To: %s',
|
| + prior,
|
| + latest)
|
| + raise ValueError('Bucket count sizes do not match')
|
| + if prior.count <= 0:
|
| + return
|
| +
|
| + old_count = latest.count
|
| + old_mean = latest.mean
|
| + old_summed_variance = latest.sumOfSquaredDeviation
|
| + bucket_counts = latest.bucketCounts
|
| +
|
| + # Update the latest
|
| + latest.count += prior.count
|
| + latest.maximum = max(prior.maximum, latest.maximum)
|
| + latest.minimum = min(prior.minimum, latest.minimum)
|
| + latest.mean = ((old_count * old_mean + prior.count * prior.mean) /
|
| + latest.count)
|
| + latest.sumOfSquaredDeviation = (
|
| + old_summed_variance + prior.sumOfSquaredDeviation +
|
| + old_count * (latest.mean - old_mean) ** 2 +
|
| + prior.count * (latest.mean - prior.mean) ** 2)
|
| + for i, (x, y) in enumerate(zip(prior.bucketCounts, bucket_counts)):
|
| + bucket_counts[i] = x + y
|
| +
|
| +_EPSILON = 1e-5
|
| +
|
| +
|
| +def _is_close_enough(x, y):
|
| + if x is None or y is None:
|
| + return False
|
| + return abs(x - y) <= _EPSILON * abs(x)
|
| +
|
| +
|
| +# This is derived from the oneof choices of the Distribution message's
|
| +# bucket_option field in google/api/servicecontrol/v1/distribution.proto, and
|
| +# should be kept in sync with that
|
| +_DISTRIBUTION_ONEOF_FIELDS = (
|
| + 'linearBuckets', 'exponentialBuckets', 'explicitBuckets')
|
| +
|
| +
|
| +def _detect_bucket_option(distribution):
|
| + for f in _DISTRIBUTION_ONEOF_FIELDS:
|
| + value = distribution.get_assigned_value(f)
|
| + if value is not None:
|
| + return f, value
|
| + return None, None
|
| +
|
| +
|
| +def _linear_buckets_nearly_equal(a, b):
|
| + return ((a.numFiniteBuckets == b.numFiniteBuckets) and
|
| + _is_close_enough(a.width, b.width) or
|
| + _is_close_enough(a.offset, b.offset))
|
| +
|
| +
|
| +def _exponential_buckets_nearly_equal(a, b):
|
| + return ((a.numFiniteBuckets == b.numFiniteBuckets) and
|
| + _is_close_enough(a.growthFactor, b.growthFactor) and
|
| + _is_close_enough(a.scale, b.scale))
|
| +
|
| +
|
| +def _explicit_buckets_nearly_equal(a, b):
|
| + if len(a.bounds) != len(b.bounds):
|
| + return False
|
| + for x, y in zip(a.bounds, b.bounds):
|
| + if not _is_close_enough(x, y):
|
| + return False
|
| + return True
|
| +
|
| +
|
| +def _buckets_nearly_equal(a_dist, b_dist):
|
| + """Determines whether two `Distributions` are nearly equal.
|
| +
|
| + Args:
|
| + a_dist (:class:`Distribution`): an instance
|
| + b_dist (:class:`Distribution`): another instance
|
| +
|
| + Return:
|
| + boolean: `True` if the two instances are approximately equal, otherwise
|
| + False
|
| +
|
| + """
|
| + a_type, a_buckets = _detect_bucket_option(a_dist)
|
| + b_type, b_buckets = _detect_bucket_option(b_dist)
|
| + if a_type != b_type:
|
| + return False
|
| + elif a_type == 'linearBuckets':
|
| + return _linear_buckets_nearly_equal(a_buckets, b_buckets)
|
| + elif a_type == 'exponentialBuckets':
|
| + return _exponential_buckets_nearly_equal(a_buckets, b_buckets)
|
| + elif a_type == 'explicitBuckets':
|
| + return _explicit_buckets_nearly_equal(a_buckets, b_buckets)
|
| + else:
|
| + return False
|
| +
|
| +
|
| +def _update_general_statistics(a_float, dist):
|
| + """Adds a_float to distribution, updating the statistics fields.
|
| +
|
| + Args:
|
| + a_float (float): a new value
|
| + dist (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + the Distribution being updated
|
| +
|
| + """
|
| + if not dist.count:
|
| + dist.count = 1
|
| + dist.maximum = a_float
|
| + dist.minimum = a_float
|
| + dist.mean = a_float
|
| + dist.sumOfSquaredDeviation = 0
|
| + else:
|
| + old_count = dist.count
|
| + old_mean = dist.mean
|
| + new_mean = ((old_count * old_mean) + a_float) / (old_count + 1)
|
| + delta_sum_squares = (a_float - old_mean) * (a_float - new_mean)
|
| + dist.count += 1
|
| + dist.mean = new_mean
|
| + dist.maximum = max(a_float, dist.maximum)
|
| + dist.minimum = min(a_float, dist.minimum)
|
| + dist.sumOfSquaredDeviation += delta_sum_squares
|
| +
|
| +
|
| +_BAD_UNSET_BUCKETS = 'cannot update a distribution with unset %s'
|
| +_BAD_LOW_BUCKET_COUNT = 'cannot update a distribution with a low bucket count'
|
| +
|
| +
|
| +def _update_exponential_bucket_count(a_float, dist):
|
| + """Adds `a_float` to `dist`, updating its exponential buckets.
|
| +
|
| + Args:
|
| + a_float (float): a new value
|
| + dist (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + the Distribution being updated
|
| +
|
| + Raises:
|
| + ValueError: if `dist` does not already have exponential buckets defined
|
| + ValueError: if there are not enough bucket count fields in `dist`
|
| + """
|
| + buckets = dist.exponentialBuckets
|
| + if buckets is None:
|
| + raise ValueError(_BAD_UNSET_BUCKETS % ('exponential buckets'))
|
| + bucket_counts = dist.bucketCounts
|
| + num_finite_buckets = buckets.numFiniteBuckets
|
| + if len(bucket_counts) < num_finite_buckets + 2:
|
| + raise ValueError(_BAD_LOW_BUCKET_COUNT)
|
| + scale = buckets.scale
|
| + factor = buckets.growthFactor
|
| + if (a_float <= scale):
|
| + index = 0
|
| + else:
|
| + index = 1 + int((math.log(a_float / scale) / math.log(factor)))
|
| + index = min(index, num_finite_buckets + 1)
|
| + bucket_counts[index] += 1
|
| + logger.debug('scale:%f, factor:%f, sample:%f, index:%d',
|
| + scale, factor, a_float, index)
|
| +
|
| +
|
| +def _update_linear_bucket_count(a_float, dist):
|
| + """Adds `a_float` to `dist`, updating the its linear buckets.
|
| +
|
| + Args:
|
| + a_float (float): a new value
|
| + dist (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + the Distribution being updated
|
| +
|
| + Raises:
|
| + ValueError: if `dist` does not already have linear buckets defined
|
| + ValueError: if there are not enough bucket count fields in `dist`
|
| + """
|
| + buckets = dist.linearBuckets
|
| + if buckets is None:
|
| + raise ValueError(_BAD_UNSET_BUCKETS % ('linear buckets'))
|
| + bucket_counts = dist.bucketCounts
|
| + num_finite_buckets = buckets.numFiniteBuckets
|
| + if len(bucket_counts) < num_finite_buckets + 2:
|
| + raise ValueError(_BAD_LOW_BUCKET_COUNT)
|
| + width = buckets.width
|
| + lower = buckets.offset
|
| + upper = lower + (num_finite_buckets * width)
|
| + if a_float < lower:
|
| + index = 0
|
| + elif a_float >= upper:
|
| + index = num_finite_buckets + 1
|
| + else:
|
| + index = 1 + int(((a_float - lower) / width))
|
| + bucket_counts[index] += 1
|
| + logger.debug('upper:%f, lower:%f, width:%f, sample:%f, index:%d',
|
| + upper, lower, width, a_float, index)
|
| +
|
| +
|
| +def _update_explicit_bucket_count(a_float, dist):
|
| + """Adds `a_float` to `dist`, updating its explicit buckets.
|
| +
|
| + Args:
|
| + a_float (float): a new value
|
| + dist (:class:`google.api.gen.servicecontrol_v1_messages.Distribution`):
|
| + the Distribution being updated
|
| +
|
| + Raises:
|
| + ValueError: if `dist` does not already have explict buckets defined
|
| + ValueError: if there are not enough bucket count fields in `dist`
|
| + """
|
| + buckets = dist.explicitBuckets
|
| + if buckets is None:
|
| + raise ValueError(_BAD_UNSET_BUCKETS % ('explicit buckets'))
|
| + bucket_counts = dist.bucketCounts
|
| + bounds = buckets.bounds
|
| + if len(bucket_counts) < len(bounds) + 1:
|
| + raise ValueError(_BAD_LOW_BUCKET_COUNT)
|
| + bucket_counts[bisect.bisect(bounds, a_float)] += 1
|
|
|