// FBuildStats
//------------------------------------------------------------------------------

// Includes
//------------------------------------------------------------------------------
#include "FBuildStats.h"

// FBuild
#include "Tools/FBuild/FBuildCore/FBuild.h"
#include "Tools/FBuild/FBuildCore/Graph/NodeGraph.h"
#include "Tools/FBuild/FBuildCore/Helpers/Report/Report.h"

// Core
#include "Core/Profile/Profile.h"
#include "Core/Strings/AStackString.h"
#include "Core/Tracing/Tracing.h"

// Static
//------------------------------------------------------------------------------
// For unit test count check stability we want to exclude "ExtraFiles" on CompilerNodes
/*static*/ bool FBuildStats::s_IgnoreCompilerNodeDeps( false );

// NodeCostSorter
//------------------------------------------------------------------------------
class NodeCostSorter
{
public:
    bool operator()( const Node * a, const Node * b ) const
    {
        return ( a->GetProcessingTime() > b->GetProcessingTime() );
    }
};

// CONSTRUCTOR - FBuildStats
//------------------------------------------------------------------------------
FBuildStats::FBuildStats()
    : m_NodeTimeTotalms( 0 )
    , m_NodeTimeProgressms( 0 )
    , m_TotalBuildTime( 0.0f )
    , m_TotalLocalCPUTimeMS( 0 )
    , m_TotalRemoteCPUTimeMS( 0 )
    , m_RootNode( nullptr )
{
    m_NodesByTime.SetCapacity( 100 * 1000 );
}

// CONSTRUCTOR - FBuildStats::Stats
//------------------------------------------------------------------------------
FBuildStats::Stats::Stats()
    : m_NumProcessed( 0 )
    , m_NumBuilt( 0 )
    , m_NumCacheHits( 0 )
    , m_NumCacheMisses( 0 )
    , m_NumCacheStores( 0 )
    , m_NumLightCacheHits( 0 )
    , m_NumLightCacheMisses( 0 )
    , m_NumLightCacheStores( 0 )
    , m_ProcessingTimeMS( 0 )
    , m_NumFailed( 0 )
    , m_CachingTimeMS( 0 )
{
}

// OnBuildStop
//------------------------------------------------------------------------------
void FBuildStats::OnBuildStop( const NodeGraph & nodeGraph, Node * node )
{
    m_RootNode = node;

    const bool buildOk = ( m_RootNode->GetState() == Node::UP_TO_DATE );

    const FBuildOptions & options = FBuild::Get().GetOptions();
    const bool showSummary = options.m_ShowSummary && ( !options.m_NoSummaryOnError || buildOk );
    const bool generateReport = ( options.m_ReportType.IsEmpty() == false );

    // Any output required?
    if ( showSummary || generateReport )
    {
        // do work common to -summary and -report
        GatherPostBuildStatistics( nodeGraph, node );

        // detailed build report
        if ( generateReport )
        {
            Report::Generate( options.m_ReportType, nodeGraph, *this );
        }

        // stdout summary
        if ( showSummary )
        {
            OutputSummary( options );
        }
    }
}

// GatherPostBuildStatistics
//------------------------------------------------------------------------------
void FBuildStats::GatherPostBuildStatistics( const NodeGraph & nodeGraph, Node * node )
{
    PROFILE_FUNCTION;

    // Mark nodes to track recursion
    nodeGraph.SetBuildPassTagForAllNodes( eTagStatsNotProcessed );

    // recurse and gather the per-node-type statistics
    GatherPostBuildStatisticsRecurse( node );

    NodeCostSorter ncs;
    m_NodesByTime.Sort( ncs );

    // Total the stats
    for ( uint32_t i = 0; i < Node::NUM_NODE_TYPES; ++i )
    {
        m_Totals.m_NumProcessed += m_PerTypeStats[ i ].m_NumProcessed;
        m_Totals.m_NumBuilt += m_PerTypeStats[ i ].m_NumBuilt;
        m_Totals.m_NumCacheHits += m_PerTypeStats[ i ].m_NumCacheHits;
        m_Totals.m_NumCacheMisses += m_PerTypeStats[ i ].m_NumCacheMisses;
        m_Totals.m_NumCacheStores += m_PerTypeStats[ i ].m_NumCacheStores;
        m_Totals.m_NumLightCacheHits += m_PerTypeStats[ i ].m_NumLightCacheHits;
        m_Totals.m_NumLightCacheMisses += m_PerTypeStats[ i ].m_NumLightCacheMisses;
        m_Totals.m_NumLightCacheStores += m_PerTypeStats[ i ].m_NumLightCacheStores;
        m_Totals.m_CachingTimeMS += m_PerTypeStats[ i ].m_CachingTimeMS;
    }
}

// OutputSummary
//------------------------------------------------------------------------------
void FBuildStats::OutputSummary( const FBuildOptions & options ) const
{
    PROFILE_FUNCTION;

    AStackString<4096> output;

    // Top 10 cost items
    if ( m_NodesByTime.IsEmpty() == false )
    {
        output += "--- Most Expensive ----------------------------------------------\n";
        output += "Time (s)  Name:\n";
        const size_t itemsToDisplay = Math::Min( m_NodesByTime.GetSize(), (size_t)20 );
        for ( size_t i = 0; i < itemsToDisplay; ++i )
        {
            const Node * n = m_NodesByTime[ i ];
            output.AppendFormat( "%-9.3f %s\n", (double)( (float)n->GetProcessingTime() / 1000.0f ), n->GetPrettyName().Get() );
        }
        output += "\n";
    }

    output += "--- Summary -----------------------------------------------------\n";

    // Per-Node type stats
    // NOTE: Only showing the interesting nodes
    output += "                                 /----- Cache -----\\\n";
    output += "Build:          Seen    Built   Hit     Miss    Store   CPU\n";
    for ( uint32_t i = 0; i < Node::NUM_NODE_TYPES; ++i )
    {
        // don't show nodes with no task
        const Stats & stats = m_PerTypeStats[ i ];
        if ( stats.m_NumProcessed == 0 )
        {
            continue;
        }

        const char * typeName = Node::GetTypeName( Node::Type( i ) );

        AStackString cpuTime;
        FormatTime( (float)( (double)stats.m_ProcessingTimeMS / (double)1000 ), cpuTime );

        AStackString cacheInfo;
        if ( ( stats.m_NumCacheHits + stats.m_NumCacheMisses + stats.m_NumCacheStores ) > 0 )
        {
            cacheInfo.Format( "%-8u%-8u%-8u",
                              stats.m_NumCacheHits,
                              stats.m_NumCacheMisses,
                              stats.m_NumCacheStores );
        }
        else
        {
            cacheInfo = "-       -       -       ";
        }
        output.AppendFormat( " - %-10s : %-8u%-8u%s%s\n",
                             typeName,
                             stats.m_NumProcessed,
                             stats.m_NumBuilt,
                             cacheInfo.Get(),
                             cpuTime.Get() );
    }

    // Cache stats
    if ( options.m_UseCacheRead || options.m_UseCacheWrite )
    {
        output += "Cache:\n";
        if ( options.m_UseCacheRead )
        {
            const double hits = static_cast<double>( m_Totals.m_NumCacheHits );
            const double misses = static_cast<double>( m_Totals.m_NumCacheMisses );
            const double readable = ( hits + misses );
            const double hitPerc = ( readable > 0.0 ) ? ( hits / readable * 100.0 ) : 0.0;
            output.AppendFormat( " - Hits       : %u (%2.1f %%) (%u Light)\n",
                                 m_Totals.m_NumCacheHits,
                                 static_cast<double>( hitPerc ),
                                 m_Totals.m_NumLightCacheHits );
            output.AppendFormat( " - Misses     : %u (%u Light)\n",
                                 m_Totals.m_NumCacheMisses,
                                 m_Totals.m_NumLightCacheMisses );
        }
        else
        {
            output.AppendFormat( " - Hits       : DISABLED\n" );
            output.AppendFormat( " - Misses     : DISABLED\n" );
        }
        if ( options.m_UseCacheWrite )
        {
            output.AppendFormat( " - Stores     : %u (%u Light)\n",
                                 m_Totals.m_NumCacheStores,
                                 m_Totals.m_NumLightCacheStores );
        }
        else
        {
            output.AppendFormat( " - Stores     : DISABLED\n" );
        }
    }

    // Time totals
    AStackString buffer;
    FormatTime( m_TotalBuildTime, buffer );
    output += "Time:\n";
    output.AppendFormat( " - Real       : %s\n", buffer.Get() );
    const float totalLocalCPUInSeconds = (float)( (double)m_TotalLocalCPUTimeMS / (double)1000 );
    const float totalRemoteCPUInSeconds = (float)( (double)m_TotalRemoteCPUTimeMS / (double)1000 );
    FormatTime( totalLocalCPUInSeconds, buffer );
    const float localRatio = ( totalLocalCPUInSeconds / m_TotalBuildTime );
    output.AppendFormat( " - Local CPU  : %s (%2.1f:1)\n", buffer.Get(), (double)localRatio );
    if ( options.m_AllowDistributed )
    {
        FormatTime( totalRemoteCPUInSeconds, buffer );
        const float remoteRatio = ( totalRemoteCPUInSeconds / m_TotalBuildTime );
        output.AppendFormat( " - Remote CPU : %s (%2.1f:1)\n", buffer.Get(), (double)remoteRatio );
    }
    output += "-----------------------------------------------------------------\n";

    OUTPUT( "%s", output.Get() );
}

// GatherPostBuildStatisticsRecurse
//------------------------------------------------------------------------------
void FBuildStats::GatherPostBuildStatisticsRecurse( Node * node )
{
    // have we seen this node when gathering stats?
    if ( node->GetBuildPassTag() == eTagStatsProcessed )
    {
        return;
    }
    node->SetBuildPassTag( eTagStatsProcessed );

    Node::Type nodeType = node->GetType();

    if ( node->GetType() != Node::PROXY_NODE )
    {
        Stats & stats = m_PerTypeStats[ nodeType ];
        stats.m_NumProcessed++;

        m_TotalLocalCPUTimeMS += node->GetProcessingTime();
        if ( node->GetStatFlag( Node::STATS_BUILT_REMOTE ) )
        {
            m_TotalRemoteCPUTimeMS += node->GetLastBuildTime();
        }
        stats.m_ProcessingTimeMS += node->GetProcessingTime();

        // add our node
        if ( node->GetProcessingTime() > 0 )
        {
            // don't add filenodes (too spammy)
            if ( node->GetType() != Node::FILE_NODE )
            {
                m_NodesByTime.Append( node );
            }
        }

        if ( node->GetStatFlag( Node::STATS_BUILT ) )
        {
            stats.m_NumBuilt++;
        }
        if ( node->GetStatFlag( Node::STATS_FAILED ) )
        {
            stats.m_NumFailed++;
        }
        if ( node->GetStatFlag( Node::STATS_CACHE_HIT ) )
        {
            stats.m_NumCacheHits++;
        }
        if ( node->GetStatFlag( Node::STATS_CACHE_MISS ) )
        {
            stats.m_NumCacheMisses++;
        }
        if ( node->GetStatFlag( Node::STATS_CACHE_STORE ) )
        {
            stats.m_NumCacheStores++;
            stats.m_CachingTimeMS += node->GetCachingTime();
        }
        if ( node->GetStatFlag( Node::STATS_LIGHT_CACHE ) )
        {
            if ( node->GetStatFlag( Node::STATS_CACHE_HIT ) )
            {
                stats.m_NumLightCacheHits++;
            }
            else if ( node->GetStatFlag( Node::STATS_CACHE_STORE ) )
            {
                stats.m_NumLightCacheStores++;
            }
            else
            {
                stats.m_NumLightCacheMisses++;
            }
        }
    }

    // For unit test count check stability we want to exclude "ExtraFiles" on CompilerNodes
    if ( s_IgnoreCompilerNodeDeps && ( node->GetType() == Node::COMPILER_NODE ) )
    {
        return;
    }

    // handle deps
    GatherPostBuildStatisticsRecurse( node->GetPreBuildDependencies() );
    GatherPostBuildStatisticsRecurse( node->GetStaticDependencies() );
    GatherPostBuildStatisticsRecurse( node->GetDynamicDependencies() );
}

// GatherPostBuildStatisticsRecurse
//------------------------------------------------------------------------------
void FBuildStats::GatherPostBuildStatisticsRecurse( const Dependencies & dependencies )
{
    for ( const Dependency & dep : dependencies )
    {
        GatherPostBuildStatisticsRecurse( dep.GetNode() );
    }
}

// FormatTime
//------------------------------------------------------------------------------
/*static*/ void FBuildStats::FormatTime( float timeInSeconds, AString & outBuffer )
{
    outBuffer.Clear();

    const uint32_t days = (uint32_t)( timeInSeconds / ( 24.0f * 60.0f * 60.0f ) );
    timeInSeconds -= ( (float)days * ( 24.0f * 60.0f * 60.0f ) );
    const uint32_t hours = (uint32_t)( timeInSeconds / ( 60.0f * 60.0f ) );
    timeInSeconds -= ( (float)hours * ( 60.0f * 60.0f ) );
    const uint32_t mins = (uint32_t)( timeInSeconds / 60.0f );
    timeInSeconds -= ( (float)mins * 60.0f );

    if ( days > 0 )
    {
        outBuffer.AppendFormat( "%u days, ", days );
    }
    if ( hours > 0 )
    {
        outBuffer.AppendFormat( "%uh:", hours );
    }
    if ( mins > 0 )
    {
        outBuffer.AppendFormat( "%um ", mins );
    }

    outBuffer.AppendFormat( "%2.3fs", (double)timeInSeconds );
}

//------------------------------------------------------------------------------
