sandbox
Loading...
Searching...
No Matches
tracking_allocator.hpp
1// SPDX-License-Identifier: MIT
2#ifndef LIBSBX_MEMORY_TRACKING_ALLOCATOR_HPP_
3#define LIBSBX_MEMORY_TRACKING_ALLOCATOR_HPP_
4
5#include <memory>
6#include <vector>
7#include <string>
8#include <string_view>
9#include <list>
10#include <map>
11#include <unordered_map>
12#include <atomic>
13#include <mutex>
14#include <cstddef>
15#include <cstdlib>
16#include <new>
17#include <source_location>
18
19#include <magic_enum/magic_enum.hpp>
20
21#include <libsbx/utility/logger.hpp>
22#include <libsbx/utility/enum.hpp>
23#include <libsbx/utility/target.hpp>
24
25namespace sbx::memory {
26
28
29#if defined(SBX_MEMORY_TRACKING)
30 inline static constexpr auto value = (SBX_MEMORY_TRACKING != 0);
31#else
32 inline static constexpr auto value = utility::is_build_configuration_debug_v;
33#endif
34
35}; // struct is_memory_tracking_enabled
36
37inline constexpr auto is_memory_tracking_enabled_v = is_memory_tracking_enabled::value;
38
39enum class allocation_category : std::uint8_t {
40 general,
41 engine,
42 graphics,
43 physics,
44 audio,
45 scripting,
46 ui,
47 ai,
48 unknown
49}; // enum class allocation_category
50
51constexpr auto to_string(const allocation_category category) noexcept -> std::string_view {
52 switch (category) {
53 case allocation_category::general: return "General";
54 case allocation_category::engine: return "Engine";
55 case allocation_category::graphics: return "Graphics";
56 case allocation_category::physics: return "Physics";
57 case allocation_category::audio: return "Audio";
58 case allocation_category::scripting: return "Scripting";
59 case allocation_category::ui: return "Ui";
60 case allocation_category::ai: return "Ai";
61 case allocation_category::unknown:
62 default: return "Unknown";
63 }
64}
65
67
68 std::size_t allocation_count{0};
69 std::size_t deallocation_count{0};
70 std::size_t bytes_allocated{0};
71 std::size_t bytes_freed{0};
72 std::size_t peak_bytes{0};
73
74 [[nodiscard]] auto current_bytes() const noexcept -> std::size_t {
75 return bytes_allocated - bytes_freed;
76 }
77
78 [[nodiscard]] auto current_allocations() const noexcept -> std::size_t {
79 return allocation_count - deallocation_count;
80 }
81
82}; // struct allocation_statistics_snapshot
83
85
86 std::atomic<std::size_t> allocation_count{0};
87 std::atomic<std::size_t> deallocation_count{0};
88 std::atomic<std::size_t> bytes_allocated{0};
89 std::atomic<std::size_t> bytes_freed{0};
90 std::atomic<std::size_t> peak_bytes{0};
91
92 [[nodiscard]] auto snapshot() const noexcept -> allocation_statistics_snapshot {
94
95 out.allocation_count = allocation_count.load(std::memory_order_relaxed);
96 out.deallocation_count = deallocation_count.load(std::memory_order_relaxed);
97 out.bytes_allocated = bytes_allocated.load(std::memory_order_relaxed);
98 out.bytes_freed = bytes_freed.load(std::memory_order_relaxed);
99 out.peak_bytes = peak_bytes.load(std::memory_order_relaxed);
100
101 return out;
102 }
103
104 [[nodiscard]] auto current_bytes() const noexcept -> std::size_t {
105 return bytes_allocated.load(std::memory_order_relaxed) - bytes_freed.load(std::memory_order_relaxed);
106 }
107
108 [[nodiscard]] auto current_allocations() const noexcept -> std::size_t {
109 return allocation_count.load(std::memory_order_relaxed) - deallocation_count.load(std::memory_order_relaxed);
110 }
111
112}; // struct allocation_statistics
113
114struct alignas(std::max_align_t) allocation_header {
115
116 inline static constexpr auto magic_allocated = std::uint32_t{0xABCD1234};
117 inline static constexpr auto magic_freed = std::uint32_t{0xDEADBEEF};
118
119 std::size_t size;
120 std::size_t alignment;
121 allocation_category category;
122 std::uint32_t source_line;
123 const char* source_file;
124 allocation_header* previous;
125 allocation_header* next;
126 std::uint32_t magic;
127
128}; // struct allocation_header
129
131public:
132
133 static auto instance() noexcept -> memory_tracker& {
134 static auto tracker = memory_tracker{};
135
136 return tracker;
137 }
138
139 void record_allocation(allocation_header* header) noexcept {
140 auto& statistics = _statistics[utility::to_underlying(header->category)];
141
142 statistics.allocation_count.fetch_add(1, std::memory_order_relaxed);
143
144 auto old_bytes = statistics.bytes_allocated.fetch_add(header->size, std::memory_order_relaxed);
145
146 auto current = old_bytes + header->size - statistics.bytes_freed.load(std::memory_order_relaxed);
147 auto peak = statistics.peak_bytes.load(std::memory_order_relaxed);
148
149 while (current > peak && !statistics.peak_bytes.compare_exchange_weak(peak, current, std::memory_order_relaxed)) { }
150
151 {
152 auto lock = std::lock_guard{_statistics_mutex};
153
154 header->previous = nullptr;
155 header->next = _head;
156
157 if (_head) {
158 _head->previous = header;
159 }
160
161 _head = header;
162 }
163 }
164
165 void record_deallocation(allocation_header* header) noexcept {
166 auto& statistics = _statistics[utility::to_underlying(header->category)];
167
168 statistics.deallocation_count.fetch_add(1, std::memory_order_relaxed);
169 statistics.bytes_freed.fetch_add(header->size, std::memory_order_relaxed);
170
171 {
172 auto lock = std::lock_guard{_statistics_mutex};
173
174 if (header->previous) {
175 header->previous->next = header->next;
176 } else {
177 _head = header->next;
178 }
179 if (header->next) {
180 header->next->previous = header->previous;
181 }
182 }
183 }
184
185 [[nodiscard]] auto statistics(const allocation_category category) const noexcept -> allocation_statistics_snapshot {
186 return _statistics[utility::to_underlying(category)].snapshot();
187 }
188
189 [[nodiscard]] auto total_statistics() const noexcept -> allocation_statistics_snapshot {
190 auto total = allocation_statistics_snapshot{};
191
192 for (const auto& statistics : _statistics) {
193 auto snapshot = statistics.snapshot();
194
195 total.allocation_count += snapshot.allocation_count;
196 total.deallocation_count += snapshot.deallocation_count;
197 total.bytes_allocated += snapshot.bytes_allocated;
198 total.bytes_freed += snapshot.bytes_freed;
199 total.peak_bytes = std::max(total.peak_bytes, snapshot.peak_bytes);
200 }
201
202 return total;
203 }
204
205 auto report_leaks() const -> void {
206 auto lock = std::lock_guard{_statistics_mutex};
207
208 if (!_head) {
209 return;
210 }
211
212 auto leak_count = std::size_t{0};
213 auto leak_bytes = std::size_t{0};
214
215 for (auto* header = _head; header; header = header->next) {
216 ++leak_count;
217 leak_bytes += header->size;
218
219 utility::logger<"memory">::warn("Leak: {} bytes at {}:{}", header->size, header->source_file, header->source_line);
220 }
221
222 utility::logger<"memory">::warn("Memory leaks detected: {} allocations, {} bytes", leak_count, leak_bytes);
223 }
224
225private:
226
227 memory_tracker() = default;
228
229 memory_tracker(const memory_tracker&) = delete;
230
232 // report_leaks();
233 }
234
235 auto operator=(const memory_tracker&) -> memory_tracker& = delete;
236
237 std::array<allocation_statistics, magic_enum::enum_count<allocation_category>()> _statistics{};
238 mutable std::mutex _statistics_mutex;
239
240 allocation_header* _head{nullptr};
241
242}; // class memory_tracker
243
244namespace detail {
245
246[[nodiscard]] inline auto aligned_allocate(std::size_t size, std::size_t alignment, const allocation_category category, std::string_view file, std::int32_t line) -> void* {
247 auto effective_alignment = alignment;
248
249 effective_alignment = std::max(effective_alignment, alignof(void*));
250 effective_alignment = std::max(effective_alignment, alignof(std::max_align_t));
251
252 if (!std::has_single_bit(effective_alignment)) {
253 effective_alignment = std::bit_ceil(effective_alignment);
254 }
255
256 auto overhead = sizeof(allocation_header) + sizeof(allocation_header*) + (effective_alignment - 1);
257 auto total_size = size + overhead;
258
259 auto* base = std::malloc(total_size);
260
261 if (!base) {
262 throw std::bad_alloc{};
263 }
264
265 auto* base_bytes = static_cast<std::byte*>(base);
266
267 auto* header = std::construct_at(reinterpret_cast<allocation_header*>(base_bytes));
268 header->size = size;
269 header->alignment = effective_alignment;
270 header->category = category;
271 header->source_line = static_cast<std::uint32_t>(line);
272 header->source_file = file.data();
273 header->previous = nullptr;
274 header->next = nullptr;
275 header->magic = allocation_header::magic_allocated;
276
277 auto* user_unaligned = base_bytes + sizeof(allocation_header) + sizeof(allocation_header*);
278
279 auto user_addr = reinterpret_cast<std::uintptr_t>(user_unaligned);
280 auto aligned_addr = (user_addr + effective_alignment - 1) & ~(static_cast<std::uintptr_t>(effective_alignment) - 1);
281
282 auto* user_ptr = reinterpret_cast<std::byte*>(aligned_addr);
283
284 auto* header_slot = reinterpret_cast<allocation_header**>(user_ptr - sizeof(allocation_header*));
285 *header_slot = header;
286
287 memory_tracker::instance().record_allocation(header);
288
289 return static_cast<void*>(user_ptr);
290}
291
292inline auto aligned_deallocate(void* ptr) noexcept -> void {
293 if (!ptr) {
294 return;
295 }
296
297 auto* user_bytes = static_cast<std::byte*>(ptr);
298 auto* header_slot = reinterpret_cast<allocation_header**>(user_bytes - sizeof(allocation_header*));
299 auto* header = *header_slot;
300
301 if (!header) {
302 std::terminate();
303 }
304
305 if (header->magic != allocation_header::magic_allocated) {
306 std::terminate();
307 }
308
309 header->magic = allocation_header::magic_freed;
310
311 memory_tracker::instance().record_deallocation(header);
312
313 std::destroy_at(header);
314 std::free(static_cast<void*>(header));
315}
316
317} // namespace detail
318
319template<typename Type, allocation_category Category = allocation_category::general>
321
322public:
323
324 using value_type = Type;
325 using pointer = Type*;
326 using const_pointer = const Type*;
327 using size_type = std::size_t;
328 using difference_type = std::ptrdiff_t;
329
330 template<typename OtherType>
331 struct rebind {
333 }; // struct rebind
334
335 using is_always_equal = std::true_type;
336 using propagate_on_container_move_assignment = std::true_type;
337
338 tracking_allocator() noexcept = default;
339
340 template<typename OtherType, allocation_category OtherCategory>
341 tracking_allocator(const tracking_allocator<OtherType, OtherCategory>&) noexcept {}
342
343 [[nodiscard]] auto allocate(size_type size, const std::source_location& source_location = std::source_location::current()) -> pointer {
344 if (size > std::numeric_limits<size_type>::max() / sizeof(Type)) {
345 throw std::bad_array_new_length{};
346 }
347
348 return static_cast<pointer>(detail::aligned_allocate(size * sizeof(Type), alignof(Type), Category, source_location.file_name(), static_cast<std::int32_t>(source_location.line())));
349 }
350
351 auto deallocate(pointer ptr, size_type) noexcept -> void {
352 detail::aligned_deallocate(ptr);
353 }
354
355}; // class tracking_allocator
356
357template<typename LhsType, allocation_category LhsCategory, typename RhsType, allocation_category RhsCategory>
358[[nodiscard]] constexpr auto operator==(const tracking_allocator<LhsType, LhsCategory>&, const tracking_allocator<RhsType, RhsCategory>&) noexcept -> bool {
359 return true;
360}
361
362template<typename Type>
363using general_tracking_allocator = tracking_allocator<Type, allocation_category::general>;
364
366
367public:
368
369 struct context {
370 allocation_category category;
371 const char* file_name;
372 std::uint32_t line;
373 }; // struct context
374
375 explicit allocation_scope(allocation_category category, const std::source_location& source_location = std::source_location::current()) noexcept
376 : _previous(_current) {
377 _current.category = category;
378 _current.file_name = source_location.file_name();
379 _current.line = static_cast<std::uint32_t>(source_location.line());
380 }
381
382 ~allocation_scope() noexcept {
383 _current = _previous;
384 }
385
386 static auto current() noexcept -> const context& {
387 return _current;
388 }
389
390private:
391
392 static thread_local context _current;
393
394 context _previous;
395
396}; // class allocation_scope
397
398#define SBX_MEMORY_SCOPE(category) auto scope_##__LINE__ = sbx::memory::allocation_scope{category, std::source_location::current()}
399
400} // namespace sbx::memory
401
402#endif // LIBSBX_MEMORY_TRACKING_ALLOCATOR_HPP_
Definition: tracking_allocator.hpp:365
Definition: tracking_allocator.hpp:130
Definition: tracking_allocator.hpp:320
Definition: logger.hpp:124
Definition: tracking_allocator.hpp:114
Definition: tracking_allocator.hpp:369
Definition: tracking_allocator.hpp:66
Definition: tracking_allocator.hpp:84
Definition: tracking_allocator.hpp:27
Definition: tracking_allocator.hpp:331