/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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
 *
 *      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 "ATraceMemoryDump.h"

#include <utils/Trace.h>

#include <cstring>

namespace android {
namespace uirenderer {
namespace skiapipeline {

// When purgeable is INVALID_MEMORY_SIZE it won't be logged at all.
#define INVALID_MEMORY_SIZE -1

/**
 * Skia invokes the following SkTraceMemoryDump functions:
 * 1. dumpNumericValue (dumpName, units="bytes", valueName="size")
 * 2. dumpStringValue (dumpName, valueName="type") [optional -> for example CPU memory does not
 * invoke dumpStringValue]
 * 3. dumpNumericValue (dumpName, units="bytes", valueName="purgeable_size") [optional]
 * 4. setMemoryBacking(dumpName, backingType) [optional -> for example Vulkan GPU resources do not
 * invoke setMemoryBacking]
 *
 * ATraceMemoryDump calculates memory category first by looking at the "type" string passed to
 * dumpStringValue and then by looking at "backingType" passed to setMemoryBacking.
 * Only GPU Texture memory is tracked separately and everything else is grouped as one
 * "Misc Memory" category.
 */
static std::unordered_map<const char*, const char*> sResourceMap = {
        {"malloc", "HWUI CPU Memory"},          // taken from setMemoryBacking(backingType)
        {"gl_texture", "HWUI Texture Memory"},  // taken from setMemoryBacking(backingType)
        {"Texture", "HWUI Texture Memory"},  // taken from dumpStringValue(value, valueName="type")
        // Uncomment categories below to split "Misc Memory" into more brackets for debugging.
        /*{"vk_buffer", "vk_buffer"},
        {"gl_renderbuffer", "gl_renderbuffer"},
        {"gl_buffer", "gl_buffer"},
        {"RenderTarget", "RenderTarget"},
        {"Stencil", "Stencil"},
        {"Path Data", "Path Data"},
        {"Buffer Object", "Buffer Object"},
        {"Surface", "Surface"},*/
};

ATraceMemoryDump::ATraceMemoryDump() {
    mLastDumpName.reserve(100);
    mCategory.reserve(100);
}

void ATraceMemoryDump::dumpNumericValue(const char* dumpName, const char* valueName,
                                        const char* units, uint64_t value) {
    if (!strcmp(units, "bytes")) {
        recordAndResetCountersIfNeeded(dumpName);
        if (!strcmp(valueName, "size")) {
            mLastDumpValue = value;
        } else if (!strcmp(valueName, "purgeable_size")) {
            mLastPurgeableDumpValue = value;
        }
    }
}

void ATraceMemoryDump::dumpStringValue(const char* dumpName, const char* valueName,
                                       const char* value) {
    if (!strcmp(valueName, "type")) {
        recordAndResetCountersIfNeeded(dumpName);
        auto categoryIt = sResourceMap.find(value);
        if (categoryIt != sResourceMap.end()) {
            mCategory = categoryIt->second;
        }
    }
}

void ATraceMemoryDump::setMemoryBacking(const char* dumpName, const char* backingType,
                                        const char* backingObjectId) {
    recordAndResetCountersIfNeeded(dumpName);
    auto categoryIt = sResourceMap.find(backingType);
    if (categoryIt != sResourceMap.end()) {
        mCategory = categoryIt->second;
    }
}

/**
 * startFrame is invoked before dumping anything. It resets counters from the previous frame.
 * This is important, because if there is no new data for a given category trace would assume
 * usage has not changed (instead of reporting 0).
 */
void ATraceMemoryDump::startFrame() {
    resetCurrentCounter("");
    for (auto& it : mCurrentValues) {
        // Once a category is observed in at least one frame, it is always reported in subsequent
        // frames (even if it is 0). Not logging a category to ATRACE would mean its value has not
        // changed since the previous frame, which is not what we want.
        it.second.memory = 0;
        // If purgeableMemory is INVALID_MEMORY_SIZE, then logTraces won't log it at all.
        if (it.second.purgeableMemory != INVALID_MEMORY_SIZE) {
            it.second.purgeableMemory = 0;
        }
    }
}

/**
 * logTraces reads from mCurrentValues and logs the counters with ATRACE.
 */
void ATraceMemoryDump::logTraces() {
    // Accumulate data from last dumpName
    recordAndResetCountersIfNeeded("");
    uint64_t hwui_all_frame_memory = 0;
    for (auto& it : mCurrentValues) {
        hwui_all_frame_memory += it.second.memory;
        ATRACE_INT64(it.first.c_str(), it.second.memory);
        if (it.second.purgeableMemory != INVALID_MEMORY_SIZE) {
            ATRACE_INT64((std::string("Purgeable ") + it.first).c_str(), it.second.purgeableMemory);
        }
    }
    ATRACE_INT64("HWUI All Memory", hwui_all_frame_memory);
}

/**
 * recordAndResetCountersIfNeeded reads memory usage from mLastDumpValue/mLastPurgeableDumpValue and
 * accumulates in mCurrentValues[category]. It makes provision to create a new category and track
 * purgeable memory only if there is at least one observation.
 * recordAndResetCountersIfNeeded won't do anything until all the information for a given dumpName
 * is received.
 */
void ATraceMemoryDump::recordAndResetCountersIfNeeded(const char* dumpName) {
    if (!mLastDumpName.compare(dumpName)) {
        // Still waiting for more data for current dumpName.
        return;
    }

    // First invocation will have an empty mLastDumpName.
    if (!mLastDumpName.empty()) {
        // A new dumpName observed -> store the data already collected.
        auto memoryCounter = mCurrentValues.find(mCategory);
        if (memoryCounter != mCurrentValues.end()) {
            memoryCounter->second.memory += mLastDumpValue;
            if (mLastPurgeableDumpValue != INVALID_MEMORY_SIZE) {
                if (memoryCounter->second.purgeableMemory == INVALID_MEMORY_SIZE) {
                    memoryCounter->second.purgeableMemory = mLastPurgeableDumpValue;
                } else {
                    memoryCounter->second.purgeableMemory += mLastPurgeableDumpValue;
                }
            }
        } else {
            mCurrentValues[mCategory] = {mLastDumpValue, mLastPurgeableDumpValue};
        }
    }

    // Reset counters and default category for the newly observed "dumpName".
    resetCurrentCounter(dumpName);
}

void ATraceMemoryDump::resetCurrentCounter(const char* dumpName) {
    mLastDumpValue = 0;
    mLastPurgeableDumpValue = INVALID_MEMORY_SIZE;
    mLastDumpName = dumpName;
    // Categories not listed in sResourceMap are reported as "Misc Memory"
    mCategory = "HWUI Misc Memory";
}

} /* namespace skiapipeline */
} /* namespace uirenderer */
} /* namespace android */