# Copyright 2015 Amazon.com, Inc. or its affiliates. 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. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file 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.
import sysconfig

from tests import BaseClientDriverTest


class TestDoesNotLeakMemory(BaseClientDriverTest):
    # The user doesn't need to have credentials configured
    # in order to run the functional tests for resource leaks.
    # If we don't set this value and a user doesn't have creds
    # configured, each create_client() call will have to go through
    # the EC2 Instance Metadata provider's timeout, which can add
    # a substantial amount of time to the total test run time.
    INJECT_DUMMY_CREDS = True
    # We're making up numbers here, but let's say arbitrarily
    # that the memory can't increase by more than {SCALING_FACTOR} MBs.

    if sysconfig.get_config_var("Py_GIL_DISABLED") == 1:
        # Free-threaded Python objects require additional
        # memory for per-object locks and synchronization.
        SCALING_FACTOR = 17
    else:
        SCALING_FACTOR = 4
    MAX_GROWTH_BYTES = SCALING_FACTOR * 1024 * 1024

    def test_create_single_client_memory_constant(self):
        self.cmd('create_client', 's3')
        self.cmd('free_clients')
        self.record_memory()
        for _ in range(100):
            self.cmd('create_client', 's3')
            self.cmd('free_clients')
        self.record_memory()
        start, end = self.memory_samples
        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))

    def test_create_memory_clients_in_loop(self):
        # We need to first create clients and free then before
        # recording our memory samples.  This is because of two reasons:
        # 1. Caching.  Some of the botocore internals will cache data, so
        #    the first client created will consume more memory than subsequent
        #    clients.  We're interested in growing memory, not total
        #    memory usage (for now), so we we care about the memory in the
        #    steady state case.
        # 2. Python memory allocation.  Due to how python allocates memory
        #    via it's small object allocator, arena's aren't freed until the
        #    entire 256kb isn't in use.  If a single allocation in a single
        #    pool in a single arena is still in use, the arena is not
        #    freed.  This case is easy to hit, and pretty much any
        #    fragmentation guarantees this case is hit.  The best we can
        #    do is verify that memory that's released back to python's
        #    allocator (but not to the OS) is at least reused in subsequent
        #    requests to create botocore clients.
        self.cmd('create_multiple_clients', '200', 's3')
        self.cmd('free_clients')
        self.record_memory()
        # 500 clients in batches of 50.
        for _ in range(10):
            self.cmd('create_multiple_clients', '50', 's3')
            self.cmd('free_clients')
        self.record_memory()
        start, end = self.memory_samples
        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))

    def test_create_single_waiter_memory_constant(self):
        self.cmd('create_waiter', 's3', 'bucket_exists')
        self.cmd('free_waiters')
        self.record_memory()
        for _ in range(100):
            self.cmd('create_waiter', 's3', 'bucket_exists')
            self.cmd('free_waiters')
        self.record_memory()
        start, end = self.memory_samples
        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))

    def test_create_memory_waiters_in_loop(self):
        # See ``test_create_memory_clients_in_loop`` to understand why
        # waiters are first initialized and then freed. Same reason applies.
        self.cmd('create_multiple_waiters', '200', 's3', 'bucket_exists')
        self.cmd('free_waiters')
        self.record_memory()
        # 500 waiters in batches of 50.
        for _ in range(10):
            self.cmd('create_multiple_waiters', '50', 's3', 'bucket_exists')
            self.cmd('free_waiters')
        self.record_memory()
        start, end = self.memory_samples
        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))

    def test_create_single_paginator_memory_constant(self):
        self.cmd('create_paginator', 's3', 'list_objects')
        self.cmd('free_paginators')
        self.record_memory()
        for _ in range(100):
            self.cmd('create_paginator', 's3', 'list_objects')
            self.cmd('free_paginators')
        self.record_memory()
        start, end = self.memory_samples
        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))

    def test_create_memory_paginators_in_loop(self):
        # See ``test_create_memory_clients_in_loop`` to understand why
        # paginators are first initialized and then freed. Same reason applies.
        self.cmd('create_multiple_paginators', '200', 's3', 'list_objects')
        self.cmd('free_paginators')
        self.record_memory()
        # 500 waiters in batches of 50.
        for _ in range(10):
            self.cmd('create_multiple_paginators', '50', 's3', 'list_objects')
            self.cmd('free_paginators')
        self.record_memory()
        start, end = self.memory_samples
        self.assertTrue((end - start) < self.MAX_GROWTH_BYTES, (end - start))
