agent/native/ext/MemoryTracker.cpp (382 lines of code) (raw):
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you 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.
*/
#include "MemoryTracker.h"
#if ( ELASTIC_APM_MEMORY_TRACKING_ENABLED_01 != 0 )
#define ELASTIC_APM_CURRENT_LOG_CATEGORY ELASTIC_APM_LOG_CATEGORY_MEM_TRACKER
#include <algorithm>
#include <stddef.h>
#include <string.h> // memcpy
#include "util.h"
#include "TextOutputStream.h"
#include "elastic_apm_assert.h"
#include "basic_macros.h"
#include "IntrusiveDoublyLinkedList.h"
#include "log.h"
#include "platform.h"
const char* memoryTrackingLevelNames[ numberOfMemoryTrackingLevels ] =
{
[ memoryTrackingLevel_off ] = "OFF",
[ memoryTrackingLevel_totalCountOnly ] = "total_count_only",
[ memoryTrackingLevel_eachAllocation ] = "each_allocation",
[ memoryTrackingLevel_eachAllocationWithStackTrace ] = "each_allocation_with_stack_trace",
[ memoryTrackingLevel_all ] = "ALL"
};
static const UInt32 prefixMagicExpectedValue = 0xCAFEBABE;
static const UInt32 suffixMagicExpectedValue = 0x1CEB00DA;
static const UInt32 invalidMagicValue = 0xDEADBEEF;
static constexpr size_t maxNumberOfLeakedAllocationsToReport = 10;
static const size_t maxNumberOfBytesFromLeakedAllocationToReport = 100;
struct EmbeddedTrackingDataHeader
{
UInt32 prefixMagic;
IntrusiveDoublyLinkedListNode intrusiveNode;
String fileName;
UInt lineNumber;
size_t originallyRequestedSize;
bool isString;
size_t stackTraceAddressesCount;
};
typedef struct EmbeddedTrackingDataHeader EmbeddedTrackingDataHeader;
/**
* EmbeddedTrackingDataHeaderVariadicSuffix is used only to calculate sizes of its fields.
* It symbolizes a trailing part of the EmbeddedTrackingDataHeader with variadic data (stackTraceAddresses).
*/
#pragma clang diagnostic push
#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
#pragma ide diagnostic ignored "UnusedLocalVariable"
struct EmbeddedTrackingDataHeaderVariadicSuffix
{
void* stackTraceAddresses[ 1 ]; // EmbeddedTrackingDataHeader::stackTraceAddressesCount is the actual number of elements in stackTraceAddresses
UInt32 suffixMagic;
};
#pragma clang diagnostic pop
typedef struct EmbeddedTrackingDataHeaderVariadicSuffix EmbeddedTrackingDataHeaderVariadicSuffix;
void constructMemoryTracker( MemoryTracker* memTracker )
{
ELASTIC_APM_ASSERT_VALID_PTR( memTracker );
/// Set initial level to the highest value
/// because we can only reduce memory tracking level
/// See also reconfigureMemoryTracker() in .h
memTracker->level = memoryTrackingLevel_all;
memTracker->abortOnMemoryLeak = ELASTIC_APM_MEMORY_TRACKING_DEFAULT_ABORT_ON_MEMORY_LEAK;
memTracker->allocatedPersistent = 0;
initIntrusiveDoublyLinkedList( &memTracker->allocatedPersistentBlocks );
memTracker->allocatedRequestScoped = 0;
initIntrusiveDoublyLinkedList( &memTracker->allocatedRequestScopedBlocks );
ELASTIC_APM_ASSERT_VALID_MEMORY_TRACKER( memTracker );
}
void memoryTrackerRequestInit( MemoryTracker* memTracker )
{
memTracker->allocatedRequestScoped = 0;
}
static const size_t embeddedTrackingDataAlignment = 8;
static
size_t calcSizeBeforeTrackingData( size_t originallyRequestedSize )
{
return calcAlignedSize( originallyRequestedSize, embeddedTrackingDataAlignment );
}
static
size_t calcStackTraceAddressesSize( size_t stackTraceAddressesCount )
{
return stackTraceAddressesCount * ELASTIC_APM_FIELD_SIZEOF( EmbeddedTrackingDataHeaderVariadicSuffix, stackTraceAddresses[ 0 ] );
}
size_t memoryTrackerCalcSizeToAlloc(
MemoryTracker* memTracker,
size_t originallyRequestedSize,
size_t stackTraceAddressesCount )
{
if ( memTracker->level < memoryTrackingLevel_eachAllocation ) return originallyRequestedSize;
return calcSizeBeforeTrackingData( originallyRequestedSize ) +
sizeof( EmbeddedTrackingDataHeader ) +
calcStackTraceAddressesSize( stackTraceAddressesCount ) +
ELASTIC_APM_FIELD_SIZEOF( EmbeddedTrackingDataHeaderVariadicSuffix, suffixMagic );
}
static
EmbeddedTrackingDataHeader* allocatedBlockToTrackingData( const void* allocatedBlock, size_t originallyRequestedSize )
{
return (EmbeddedTrackingDataHeader*)( ((const Byte*)allocatedBlock) + calcSizeBeforeTrackingData( originallyRequestedSize ));
}
static
const Byte* trackingDataToAllocatedBlock( const EmbeddedTrackingDataHeader* trackingData )
{
return ((const Byte*)trackingData) - calcSizeBeforeTrackingData( trackingData->originallyRequestedSize );
}
static
void addToTrackedAllocatedBlocks(
MemoryTracker* memTracker,
const void* allocatedBlock,
size_t originallyRequestedSize,
bool isPersistent,
StringView filePath,
UInt lineNumber,
bool isString,
void* const* stackTraceAddresses,
size_t stackTraceAddressesCount )
{
EmbeddedTrackingDataHeader* trackingDataHeader = allocatedBlockToTrackingData( allocatedBlock, originallyRequestedSize );
ELASTIC_APM_ZERO_STRUCT( trackingDataHeader );
UInt64* allocated = isPersistent ? &memTracker->allocatedPersistent : &memTracker->allocatedRequestScoped;
IntrusiveDoublyLinkedList* allocatedBlocks = isPersistent ? &memTracker->allocatedPersistentBlocks : &memTracker->allocatedRequestScopedBlocks;
*allocated += originallyRequestedSize;
addToIntrusiveDoublyLinkedListBack( allocatedBlocks, &trackingDataHeader->intrusiveNode );
trackingDataHeader->prefixMagic = prefixMagicExpectedValue;
trackingDataHeader->fileName = extractLastPartOfFilePathStringView( filePath ).begin;
trackingDataHeader->lineNumber = lineNumber;
trackingDataHeader->originallyRequestedSize = originallyRequestedSize;
trackingDataHeader->isString = isString;
trackingDataHeader->stackTraceAddressesCount = stackTraceAddressesCount;
Byte* postHeader = ((Byte*)trackingDataHeader) + sizeof( EmbeddedTrackingDataHeader );
if ( stackTraceAddressesCount != 0 )
{
memcpy( postHeader, stackTraceAddresses, calcStackTraceAddressesSize( stackTraceAddressesCount ) );
postHeader += calcStackTraceAddressesSize( stackTraceAddressesCount );
}
memcpy( postHeader, &suffixMagicExpectedValue, ELASTIC_APM_FIELD_SIZEOF( EmbeddedTrackingDataHeaderVariadicSuffix, suffixMagic ) );
}
void memoryTrackerAfterAlloc(
MemoryTracker* memTracker,
const void* allocatedBlock,
size_t originallyRequestedSize,
bool isPersistent,
size_t actuallyRequestedSize,
StringView filePath,
UInt lineNumber,
bool isString,
void* const* stackTraceAddresses,
size_t stackTraceAddressesCount )
{
ELASTIC_APM_ASSERT_VALID_MEMORY_TRACKER( memTracker );
ELASTIC_APM_ASSERT_GE_UINT64( actuallyRequestedSize, originallyRequestedSize );
ELASTIC_APM_ASSERT_LE_UINT64( stackTraceAddressesCount, maxCaptureStackTraceDepth );
if ( actuallyRequestedSize > originallyRequestedSize )
{
addToTrackedAllocatedBlocks(
memTracker,
allocatedBlock,
originallyRequestedSize,
isPersistent,
filePath,
lineNumber,
isString,
stackTraceAddresses,
stackTraceAddressesCount );
}
ELASTIC_APM_ASSERT_VALID_MEMORY_TRACKER( memTracker );
}
static inline
const char* allocType( bool isPersistent )
{
return isPersistent ? "persistent" : "request scoped";
}
#define ELASTIC_APM_REPORT_MEMORY_CORRUPTION_AND_ABORT( fmt, ... ) \
do { \
ELASTIC_APM_FORCE_LOG_CRITICAL( "Memory corruption detected! " fmt , ##__VA_ARGS__ ); \
elasticApmAbort(); \
} while ( 0 )
static
void verifyMagic( String desc, UInt32 actual, UInt32 expected )
{
if ( actual == expected ) return;
ELASTIC_APM_REPORT_MEMORY_CORRUPTION_AND_ABORT(
"Magic %s is different from expected. Actual: 0x%08" PRIX32 ". Expected: 0x%08" PRIX32 ".",
desc, actual, expected );
}
static
void removeFromTrackedAllocatedBlocks(
MemoryTracker* memTracker,
const void* allocatedBlock,
size_t originallyRequestedSize,
IntrusiveDoublyLinkedList* allocatedBlocks,
size_t* possibleActuallyRequestedSize )
{
if (!allocatedBlock) {
return;
}
EmbeddedTrackingDataHeader* trackingDataHeader = allocatedBlockToTrackingData( allocatedBlock, originallyRequestedSize );
verifyMagic( "prefix", trackingDataHeader->prefixMagic, prefixMagicExpectedValue );
Byte* postHeader = ((Byte*)trackingDataHeader) + sizeof( EmbeddedTrackingDataHeader );
postHeader += calcStackTraceAddressesSize( trackingDataHeader->stackTraceAddressesCount );
UInt32 actualSuffixMagic;
memcpy( &actualSuffixMagic, postHeader, sizeof( actualSuffixMagic ) );
verifyMagic( "suffix", actualSuffixMagic, suffixMagicExpectedValue );
*possibleActuallyRequestedSize =
memoryTrackerCalcSizeToAlloc(memTracker, originallyRequestedSize, trackingDataHeader->stackTraceAddressesCount );
removeCurrentNodeIntrusiveDoublyLinkedList(
nodeToIntrusiveDoublyLinkedListIterator( allocatedBlocks, &trackingDataHeader->intrusiveNode ) );
trackingDataHeader->prefixMagic = invalidMagicValue;
memcpy( postHeader, &invalidMagicValue, ELASTIC_APM_FIELD_SIZEOF( EmbeddedTrackingDataHeaderVariadicSuffix, suffixMagic ) );
}
void memoryTrackerBeforeFree(
MemoryTracker* memTracker,
const void* allocatedBlock,
size_t originallyRequestedSize,
bool isPersistent,
size_t* possibleActuallyRequestedSize )
{
ELASTIC_APM_UNUSED( allocatedBlock );
ELASTIC_APM_ASSERT_VALID_PTR( possibleActuallyRequestedSize );
UInt64* allocated = isPersistent ? &memTracker->allocatedPersistent : &memTracker->allocatedRequestScoped;
IntrusiveDoublyLinkedList* allocatedBlocks = isPersistent ? &memTracker->allocatedPersistentBlocks : &memTracker->allocatedRequestScopedBlocks;
ELASTIC_APM_ASSERT( *allocated >= originallyRequestedSize
, "Attempting to free more %s memory than allocated. Allocated: %" PRIu64 ". Attempting to free: %" PRIu64
, allocType( isPersistent ), *allocated, (UInt64)originallyRequestedSize );
*possibleActuallyRequestedSize = originallyRequestedSize;
// Since memory tracking level can only be reduced it means that
// if the current level (i.e., at the moment of call to free()) includes tracking for each allocation
// then the level at the moment of call to malloc() included tracking for each allocation as well
if ( memTracker->level >= memoryTrackingLevel_eachAllocation )
removeFromTrackedAllocatedBlocks( memTracker, allocatedBlock, originallyRequestedSize, allocatedBlocks, possibleActuallyRequestedSize );
*allocated -= originallyRequestedSize;
}
static
const EmbeddedTrackingDataHeader* fromIntrusiveNodeToTrackingData( const IntrusiveDoublyLinkedListNode* intrusiveListNode )
{
return (const EmbeddedTrackingDataHeader*)( ((const Byte*) intrusiveListNode ) - offsetof( EmbeddedTrackingDataHeader, intrusiveNode ) );
}
static
void streamMemoryBlockAsString(
const Byte* memBlock,
size_t numberOfItemsToStream,
TextOutputStream* txtOutStream )
{
streamString( "`", txtOutStream );
String memBlockAsString = (String)memBlock;
char bufferToEscape[ escapeNonPrintableCharBufferSize ];
ELASTIC_APM_FOR_EACH_INDEX( i, numberOfItemsToStream )
streamPrintf( txtOutStream, "%s", escapeNonPrintableChar( memBlockAsString[ i ], bufferToEscape ) );
streamString( "'", txtOutStream );
}
static
void streamMemoryBlockAsBinary(
const Byte* memBlock,
size_t numberOfItemsToStream,
TextOutputStream* txtOutStream )
{
streamString( "[", txtOutStream );
ELASTIC_APM_FOR_EACH_INDEX( i, numberOfItemsToStream )
streamPrintf( txtOutStream, " %02X", (UInt)( memBlock[ i ] ) );
streamString( " ]", txtOutStream );
}
static
String streamMemBlockContent(
TextOutputStream* txtOutStream,
const EmbeddedTrackingDataHeader* trackingDataHeader )
{
TextOutputStreamState txtOutStreamStateOnEntryStart;
if ( ! textOutputStreamStartEntry( txtOutStream, &txtOutStreamStateOnEntryStart ) )
return ELASTIC_APM_TEXT_OUTPUT_STREAM_NOT_ENOUGH_SPACE_MARKER;
const Byte* memBlock = trackingDataToAllocatedBlock( trackingDataHeader );
const size_t numberOfItemsToStream =
std::min( trackingDataHeader->originallyRequestedSize, maxNumberOfBytesFromLeakedAllocationToReport );
const String itemsName = trackingDataHeader->isString ? "chars" : "bytes, in hex";
if ( numberOfItemsToStream < trackingDataHeader->originallyRequestedSize )
streamPrintf( txtOutStream, "(first %" PRIu64 " %s) ", (UInt64)numberOfItemsToStream, itemsName );
if ( trackingDataHeader->isString )
streamMemoryBlockAsString( memBlock, numberOfItemsToStream, txtOutStream );
else
streamMemoryBlockAsBinary( memBlock, numberOfItemsToStream, txtOutStream );
return textOutputStreamEndEntry( &txtOutStreamStateOnEntryStart, txtOutStream );
}
static
String streamAllocCallStackTrace(
TextOutputStream* txtOutStream,
const EmbeddedTrackingDataHeader* trackingDataHeader )
{
if ( trackingDataHeader->stackTraceAddressesCount == 0 )
return "\t\t<STACK TRACE IS NOT CAPTURED>";
TextOutputStreamState txtOutStreamStateOnEntryStart;
if ( ! textOutputStreamStartEntry( txtOutStream, &txtOutStreamStateOnEntryStart ) )
return ELASTIC_APM_TEXT_OUTPUT_STREAM_NOT_ENOUGH_SPACE_MARKER;
Byte* postHeader = ((Byte*)trackingDataHeader) + sizeof( EmbeddedTrackingDataHeader );
ELASTIC_APM_ASSERT_LE_UINT64( trackingDataHeader->stackTraceAddressesCount, maxCaptureStackTraceDepth );
void* stackTraceAddresses[ maxCaptureStackTraceDepth ];
memcpy( stackTraceAddresses, postHeader, sizeof( void* ) * trackingDataHeader->stackTraceAddressesCount );
streamStackTrace(
&(stackTraceAddresses[ 0 ]),
trackingDataHeader->stackTraceAddressesCount,
/* linePrefix: */ "\t\t",
txtOutStream );
return textOutputStreamEndEntry( &txtOutStreamStateOnEntryStart, txtOutStream );
}
static
void reportAllocation( const IntrusiveDoublyLinkedListNode* intrusiveListNode, size_t allocationIndex, size_t numberOfAllocations )
{
const EmbeddedTrackingDataHeader* trackingDataHeader = fromIntrusiveNodeToTrackingData( intrusiveListNode );
char txtOutStreamBuf[ ELASTIC_APM_TEXT_OUTPUT_STREAM_ON_STACK_BUFFER_SIZE * 10 ];
TextOutputStream txtOutStream = ELASTIC_APM_TEXT_OUTPUT_STREAM_FROM_STATIC_BUFFER( txtOutStreamBuf );
ELASTIC_APM_FORCE_LOG_CRITICAL(
"Allocation #%" PRIu64 " (out of %" PRIu64 "):"
" Source location: %s:%u. Originally requested allocation size: %" PRIu64 "."
" Content: %s.\n"
"\t+-> Allocation call stack trace:\n%s",
(UInt64)(allocationIndex + 1), (UInt64)numberOfAllocations,
trackingDataHeader->fileName, trackingDataHeader->lineNumber,
(UInt64)trackingDataHeader->originallyRequestedSize,
streamMemBlockContent( &txtOutStream, trackingDataHeader ),
streamAllocCallStackTrace( &txtOutStream, trackingDataHeader ) );
}
#ifdef ELASTIC_APM_ON_MEMORY_LEAK_CUSTOM_FUNC
// Declare to avoid warnings
void ELASTIC_APM_ON_MEMORY_LEAK_CUSTOM_FUNC();
#endif
static
void verifyBalanceIsZero( const MemoryTracker* memTracker, String whenDesc, UInt64 allocated, bool isPersistent )
{
if ( allocated == 0 )
{
return;
}
const IntrusiveDoublyLinkedList* allocatedBlocks = isPersistent ? &memTracker->allocatedPersistentBlocks : &memTracker->allocatedRequestScopedBlocks;
const size_t numberOfAllocations = calcIntrusiveDoublyLinkedListSize( allocatedBlocks );
const size_t numberOfAllocationsToReport = std::min(numberOfAllocations, maxNumberOfLeakedAllocationsToReport);
const IntrusiveDoublyLinkedListNode* allocationsToReport[ maxNumberOfLeakedAllocationsToReport ];
// Copy allocation nodes we are going to report
// because the code below might do more allocations
{
size_t allocationIndex = 0;
ELASTIC_APM_FOR_EACH_IN_INTRUSIVE_LINKED_LIST( allocationsIt, allocatedBlocks )
{
allocationsToReport[ allocationIndex++ ] = currentNodeIntrusiveDoublyLinkedList( allocationsIt );
if ( allocationIndex == numberOfAllocationsToReport ) break;
}
}
ELASTIC_APM_FORCE_LOG_CRITICAL(
"Memory leak detected! On %s amount of allocated %s memory should be 0, instead it is %" PRIu64 ,
whenDesc, allocType( isPersistent ), allocated );
ELASTIC_APM_FORCE_LOG_CRITICAL(
"Number of allocations not freed: %" PRIu64 ". Following are the first %" PRIu64 " not freed allocation(s)", (UInt64) numberOfAllocations, (UInt64) numberOfAllocationsToReport );
ELASTIC_APM_FOR_EACH_INDEX( allocationIndex, numberOfAllocationsToReport)
reportAllocation( allocationsToReport[ allocationIndex ], allocationIndex, numberOfAllocations );
#ifdef ELASTIC_APM_ON_MEMORY_LEAK_CUSTOM_FUNC
ELASTIC_APM_ON_MEMORY_LEAK_CUSTOM_FUNC();
#else
if ( memTracker->abortOnMemoryLeak )
{
ELASTIC_APM_FORCE_LOG_CRITICAL("Aborting on memory leak...");
elasticApmAbort();
}
#endif
}
void memoryTrackerRequestShutdown( MemoryTracker* memTracker )
{
verifyBalanceIsZero(
memTracker,
"request shutdown",
memTracker->allocatedRequestScoped,
/* isPersistent */ false );
}
void destructMemoryTracker( MemoryTracker* memTracker )
{
if ( isMemoryTrackingEnabled( memTracker ) )
{
verifyBalanceIsZero(
memTracker,
"module shutdown",
memTracker->allocatedPersistent,
/* isPersistent */ true );
}
memTracker->level = memoryTrackingLevel_off;
}
MemoryTrackingLevel internalChecksToMemoryTrackingLevel( InternalChecksLevel internalChecksLevel )
{
ELASTIC_APM_STATIC_ASSERT( memoryTrackingLevel_not_set == internalChecksLevel_not_set );
ELASTIC_APM_STATIC_ASSERT( numberOfMemoryTrackingLevels <= numberOfInternalChecksLevels );
ELASTIC_APM_ASSERT_IN_INCLUSIVE_RANGE_UINT64( internalChecksLevel_not_set, internalChecksLevel, internalChecksLevel_all );
if ( internalChecksLevel >= internalChecksLevel_all ) return memoryTrackingLevel_all;
if ( internalChecksLevel < ( memoryTrackingLevel_all - 1 ) ) return (MemoryTrackingLevel)internalChecksLevel;
return (MemoryTrackingLevel)( memoryTrackingLevel_all - 1 );
}
String streamMemoryTrackingLevel( MemoryTrackingLevel level, TextOutputStream* txtOutStream )
{
if ( level == memoryTrackingLevel_not_set )
return streamStringView( ELASTIC_APM_STRING_LITERAL_TO_VIEW( "not_set" ), txtOutStream );
if ( level >= numberOfMemoryTrackingLevels )
return streamInt( level, txtOutStream );
return streamString( memoryTrackingLevelNames[ level ], txtOutStream );
}
#endif // #if ( ELASTIC_APM_MEMORY_TRACKING_ENABLED_01 != 0 )