blob: 2b86d8fc3ff04140bf6e95dc63ebb2bfb62699fa [file] [log] [blame]
// Copyright 2023 The Pigweed Authors
//
// 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
//
// https://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.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include "pw_allocator/allocator.h"
#include "pw_allocator/capability.h"
#include "pw_allocator/metrics.h"
#include "pw_assert/assert.h"
#include "pw_metric/metric.h"
#include "pw_preprocessor/compiler.h"
#include "pw_result/result.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
namespace pw::allocator {
/// This tag type is used to explicitly select the constructor which adds
/// the tracking allocator's metrics group as a child of the info
/// allocator it is wrapping.
static constexpr struct AddTrackingAllocatorAsChild {
} kAddTrackingAllocatorAsChild = {};
/// Wraps an `Allocator` and records details of its usage.
///
/// Metric collection is performed using the provided template parameter type.
/// Callers can not instantiate this class directly, as it lacks a public
/// constructor. Instead, callers should use derived classes which provide the
/// template parameter type, such as `TrackingAllocator` which uses the
/// default metrics implementation, or `TrackingAllocatorForTest` which
/// always uses the real metrics implementation.
///
/// If the `requested_bytes` metric is enabled, then this allocator will add
/// overhead to each allocation. It uses one of two layouts:
///
/// * If the underlying allocator implements `GetAllocated`, this allocator
/// will store the requested size of an allocation after that allocation's
/// usable space. For example, assume `Layout{.size=256, .alignment=16}`,
/// `sizeof(size_t) == 4`, and that 'U' indicates "usable space". An
/// allocation might look like:
///
/// ..1f0 | UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU
/// ..... | ...
/// ..2e0 | UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU
/// ..2f0 | 00 00 01 00 .. .. .. .. .. .. .. .. .. .. .. .. // size suffix
///
/// * If the underlying allocator does NOT implement `GetAllocated`, this
/// allocator will store a `Layout` of the requested bytes and alignment
/// before the usable space. This is more expensive, as `alignment` bytes
/// must be added to keep the usable space aligned. For example, with the
/// same assumptions as beforea and 'x' indicating padding, an allocation
/// might look like:
///
/// ..1f0 | xx xx xx xx xx xx xx xx 00 00 01 00 00 00 00 10
/// ..2e0 | UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU
/// ..... | ...
/// ..2f0 | UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU UU
///
/// If the `requested_bytes` metric is disabled, no additional overhead will be
/// added.
template <typename MetricsType>
class TrackingAllocator : public Allocator {
public:
TrackingAllocator(metric::Token token, Allocator& allocator)
: Allocator(allocator.capabilities() | kImplementsGetRequestedLayout),
allocator_(allocator),
metrics_(token) {}
template <typename OtherMetrics>
TrackingAllocator(metric::Token token,
TrackingAllocator<OtherMetrics>& parent,
const AddTrackingAllocatorAsChild&)
: TrackingAllocator(token, parent) {
parent.metric_group().Add(metric_group());
}
const metric::Group& metric_group() const { return metrics_.group(); }
metric::Group& metric_group() { return metrics_.group(); }
const MetricsType& metrics() const { return metrics_.metrics(); }
private:
/// @copydoc Allocator::Allocate
void* DoAllocate(Layout layout) override;
/// @copydoc Allocator::Deallocate
void DoDeallocate(void* ptr) override;
/// @copydoc Allocator::Deallocate
void DoDeallocate(void* ptr, Layout) override { DoDeallocate(ptr); }
/// @copydoc Allocator::Resize
bool DoResize(void* ptr, size_t new_size) override;
/// @copydoc Allocator::Reallocate
void* DoReallocate(void* ptr, Layout new_layout) override;
/// @copydoc Allocator::GetCapacity
StatusWithSize DoGetCapacity() const override {
return allocator_.GetCapacity();
}
/// @copydoc Allocator::GetRequestedLayout
Result<Layout> DoGetRequestedLayout(const void* ptr) const override {
return DoGetRequestedLayoutImpl(FromUsableSpace(ptr));
}
/// Like ``DoGetRequestedLayout``, except that ``ptr`` must be a pointer
/// returned by the underlying allocator instead of by this object.
Result<Layout> DoGetRequestedLayoutImpl(const void* ptr) const;
/// @copydoc Allocator::GetUsableLayout
Result<Layout> DoGetUsableLayout(const void* ptr) const override;
/// @copydoc Allocator::GetAllocatedLayout
Result<Layout> DoGetAllocatedLayout(const void* ptr) const override;
/// @copydoc Allocator::Query
Status DoQuery(const void* ptr) const override {
return Query(allocator_, ptr);
}
/// Returns a layout that includes storage for an allocation and its stored
/// details.
Layout ReserveStorage(Layout requested);
/// Returns a pointer to an allocation from a pointer to its usable space.
template <typename PtrType = void*>
PtrType FromUsableSpace(PtrType ptr) const;
/// Returns a pointer to usable space from a pointer to an allocation.
void* ToUsableSpace(void* ptr, const Layout& requested) const;
/// Stores the requested size of an allocation in that allocation.
void SetRequested(void* ptr, const Layout& requested);
/// Returns a pointer to where the requested allocation size is stored.
void* GetRequestedStorage(const void* ptr) const;
Allocator& allocator_;
internal::Metrics<MetricsType> metrics_;
};
// Template method implementation.
namespace internal {
/// Extracts a ``Layout`` from a ``Result`` if ok, or provides a default
/// ``Layout`` otherwise.
inline Layout UnwrapLayout(const Result<Layout>& result) {
return result.ok() ? result.value() : Layout(0, 1);
}
} // namespace internal
template <typename MetricsType>
void* TrackingAllocator<MetricsType>::DoAllocate(Layout layout) {
Layout requested = layout;
void* new_ptr = allocator_.Allocate(ReserveStorage(requested));
if (new_ptr == nullptr) {
metrics_.RecordFailure(requested.size());
return nullptr;
}
Layout allocated =
internal::UnwrapLayout(GetAllocatedLayout(allocator_, new_ptr));
metrics_.IncrementAllocations();
metrics_.ModifyRequested(requested.size(), 0);
metrics_.ModifyAllocated(allocated.size(), 0);
SetRequested(new_ptr, requested);
return ToUsableSpace(new_ptr, requested);
}
template <typename MetricsType>
void TrackingAllocator<MetricsType>::DoDeallocate(void* ptr) {
ptr = FromUsableSpace(ptr);
Layout requested = internal::UnwrapLayout(DoGetRequestedLayoutImpl(ptr));
Layout allocated =
internal::UnwrapLayout(GetAllocatedLayout(allocator_, ptr));
allocator_.Deallocate(ptr);
metrics_.IncrementDeallocations();
metrics_.ModifyRequested(0, requested.size());
metrics_.ModifyAllocated(0, allocated.size());
}
template <typename MetricsType>
bool TrackingAllocator<MetricsType>::DoResize(void* ptr, size_t new_size) {
ptr = FromUsableSpace(ptr);
Layout requested = internal::UnwrapLayout(DoGetRequestedLayoutImpl(ptr));
Layout allocated =
internal::UnwrapLayout(GetAllocatedLayout(allocator_, ptr));
Layout new_requested(new_size, requested.alignment());
if (!allocator_.Resize(ptr, ReserveStorage(new_requested).size())) {
metrics_.RecordFailure(new_size);
return false;
}
Layout new_allocated =
internal::UnwrapLayout(GetAllocatedLayout(allocator_, ptr));
metrics_.IncrementResizes();
metrics_.ModifyRequested(new_requested.size(), requested.size());
metrics_.ModifyAllocated(new_allocated.size(), allocated.size());
SetRequested(ptr, new_requested);
return ToUsableSpace(ptr, new_requested);
}
template <typename MetricsType>
void* TrackingAllocator<MetricsType>::DoReallocate(void* ptr,
Layout new_layout) {
ptr = FromUsableSpace(ptr);
Layout requested = internal::UnwrapLayout(DoGetRequestedLayoutImpl(ptr));
Layout allocated =
internal::UnwrapLayout(GetAllocatedLayout(allocator_, ptr));
Layout new_requested(new_layout.size(), requested.alignment());
void* new_ptr = allocator_.Reallocate(ptr, ReserveStorage(new_requested));
if (new_ptr == nullptr) {
metrics_.RecordFailure(new_requested.size());
return nullptr;
}
metrics_.IncrementReallocations();
metrics_.ModifyRequested(new_requested.size(), requested.size());
Layout new_allocated =
internal::UnwrapLayout(GetAllocatedLayout(allocator_, new_ptr));
if (ptr != new_ptr) {
// Reallocate performed "alloc, copy, free". Increment and decrement
// seperately in order to ensure "peak" metrics are correct.
metrics_.ModifyAllocated(new_allocated.size(), 0);
metrics_.ModifyAllocated(0, allocated.size());
} else {
// Reallocate performed "resize" without additional overhead.
metrics_.ModifyAllocated(new_allocated.size(), allocated.size());
}
SetRequested(new_ptr, new_requested);
return ToUsableSpace(new_ptr, new_requested);
}
template <typename MetricsType>
Result<Layout> TrackingAllocator<MetricsType>::DoGetRequestedLayoutImpl(
const void* ptr) const {
Result<Layout> requested = GetRequestedLayout(allocator_, ptr);
if (requested.ok()) {
return requested;
}
if constexpr (has_requested_bytes<MetricsType>::value) {
Layout layout;
std::memcpy(&layout, GetRequestedStorage(ptr), sizeof(layout));
return layout;
}
return Status::Unimplemented();
}
template <typename MetricsType>
Result<Layout> TrackingAllocator<MetricsType>::DoGetUsableLayout(
const void* ptr) const {
ptr = FromUsableSpace(ptr);
Result<Layout> usable = GetUsableLayout(allocator_, ptr);
if constexpr (has_requested_bytes<MetricsType>::value) {
if (!allocator_.HasCapability(Capability::kImplementsGetRequestedLayout)) {
if (usable.ok()) {
size_t size = usable->size();
PW_ASSERT(!PW_SUB_OVERFLOW(size, sizeof(Layout), &size));
usable = Layout(size, usable->alignment());
}
}
}
return usable;
}
template <typename MetricsType>
Result<Layout> TrackingAllocator<MetricsType>::DoGetAllocatedLayout(
const void* ptr) const {
ptr = FromUsableSpace(ptr);
return GetAllocatedLayout(allocator_, ptr);
}
template <typename MetricsType>
Layout TrackingAllocator<MetricsType>::ReserveStorage(Layout requested) {
size_t size = requested.size();
size_t alignment = requested.alignment();
if constexpr (has_requested_bytes<MetricsType>::value) {
if (!allocator_.HasCapability(Capability::kImplementsGetRequestedLayout)) {
if (allocator_.HasCapability(Capability::kImplementsGetUsableLayout)) {
PW_ASSERT(!PW_ADD_OVERFLOW(size, sizeof(Layout), &size));
} else {
alignment = std::max(alignment, sizeof(Layout));
PW_ASSERT(!PW_ADD_OVERFLOW(alignment, size, &size));
}
}
}
return Layout(size, alignment);
}
template <typename MetricsType>
void* TrackingAllocator<MetricsType>::ToUsableSpace(
void* ptr, const Layout& requested) const {
auto addr = reinterpret_cast<uintptr_t>(ptr);
if constexpr (has_requested_bytes<MetricsType>::value) {
if (!allocator_.HasCapability(Capability::kImplementsGetRequestedLayout) &&
!allocator_.HasCapability(Capability::kImplementsGetUsableLayout)) {
PW_ASSERT(PW_ADD_OVERFLOW(addr, requested.alignment(), &addr));
}
}
return reinterpret_cast<void*>(addr);
}
template <typename MetricsType>
template <typename PtrType>
PtrType TrackingAllocator<MetricsType>::FromUsableSpace(PtrType ptr) const {
auto addr = reinterpret_cast<uintptr_t>(ptr);
if constexpr (has_requested_bytes<MetricsType>::value) {
if (!allocator_.HasCapability(Capability::kImplementsGetRequestedLayout) &&
!allocator_.HasCapability(Capability::kImplementsGetUsableLayout)) {
uintptr_t requested_addr;
PW_ASSERT(PW_SUB_OVERFLOW(addr, sizeof(Layout), &requested_addr));
Layout requested;
std::memcpy(
&requested, reinterpret_cast<void*>(requested_addr), sizeof(Layout));
PW_ASSERT(PW_SUB_OVERFLOW(addr, requested.alignment(), &addr));
}
}
return reinterpret_cast<PtrType>(addr);
}
template <typename MetricsType>
void TrackingAllocator<MetricsType>::SetRequested(void* ptr,
const Layout& requested) {
if constexpr (has_requested_bytes<MetricsType>::value) {
if (!allocator_.HasCapability(Capability::kImplementsGetRequestedLayout)) {
std::memcpy(GetRequestedStorage(ptr), &requested, sizeof(requested));
}
}
}
template <typename MetricsType>
void* TrackingAllocator<MetricsType>::GetRequestedStorage(
const void* ptr) const {
if constexpr (has_requested_bytes<MetricsType>::value) {
Result<Layout> usable = GetUsableLayout(allocator_, ptr);
auto addr = reinterpret_cast<uintptr_t>(ptr);
if (usable.ok()) {
PW_ASSERT(!PW_ADD_OVERFLOW(addr, usable->size(), &addr));
PW_ASSERT(!PW_SUB_OVERFLOW(addr, sizeof(Layout), &addr));
} else {
PW_ASSERT(!PW_ADD_OVERFLOW(addr, usable->alignment(), &addr));
PW_ASSERT(!PW_SUB_OVERFLOW(addr, sizeof(Layout), &addr));
}
return reinterpret_cast<void*>(addr);
} else {
// `GetRequestedStorage()` is not available when `requested_bytes` disabled.
PW_ASSERT(false);
}
}
// TODO(b/326509341): This is an interim alias to facilitate refactoring
// downstream consumers of `TrackingAllocator` to add a template parameter.
//
// The following migration steps are complete:
// 1. Downstream consumers will be updated to use `TrackingAllocatorImpl<...>`.
// 2. The iterim `TrackingAllocator` class will be removed.
// 3. `TrackingAllocatorImpl<...>` will be renamed to `TrackingAllocator<...>`,
// with a `TrackingAllocatorImpl<...>` alias pointing to it.
//
// The following migration steps remain:
// 4. Downstream consumers will be updated to use `TrackingAllocator<...>`.
// 5. The `TrackingAllocatorImpl<...>` alias will be removed.
template <typename MetricsType>
using TrackingAllocatorImpl = TrackingAllocator<MetricsType>;
} // namespace pw::allocator