Bootstrap

NanoLog起步笔记-5-客户端简要描述

客户端是大头。nanolog的服务端中规中矩,没有太多需要描述之处。但客户端相对复杂得多。

客户端的简要的设计图路

Nanolog客户端,是以线程为最小粒度设计的、压入数据不管,由server端来取走的的模式设计的。
所以,客户端有最大等待时长。超时则抛弃。

notify模式

当然,要注意,有notifiy模式,这种模式下,服务端不再是轮询。而是等待客户端通知。
在数据量小的时候、核心特别紧张时,可以考虑。
在这里插入图片描述

服务端最好分两个核

因为,默认情况下,server端,不论客户端的buffer有无数据,都要忙轮询,在外界看来,这个核 的用户占CPU占用率是100。
另外注意,如果核心相对充裕,至少要给log服务端编两个核,以免服务线程和异步io线程相互干扰。
上面这些不是我们要讲的,所以,还是到正题。

NanoLog::setLogLevel(NOTICE);

这个没什么好说的。

    // Optional: Set the minimum LogLevel that log messages must have to be
    // persisted. Valid from least to greatest values are
    // DEBUG, NOTICE, WARNING, ERROR
    NanoLog::setLogLevel(NOTICE);

不过可以看到,返回的时间很早。
这里我需要自我反省一下,最初看到这段代码,当时忙别的,没仔细看,以后是很靠后。
后来仔细看才明白,前面的话,都是在编译阶段固化的。
所以,这段是没有问题的,几乎确是在第一句就检查log打印级别。已经做到了最快。
在这里插入图片描述

从 NANO_LOG 开始

NANO_LOG

    NANO_LOG(DEBUG, "This message wont be logged since it is lower "
                        "than the current log level.");

宏展开


/**
 * NANO_LOG macro used for logging.
 *
 * \param severity
 *      The LogLevel of the log invocation (must be constant)
 * \param format
 *      printf-like format string (must be literal)
 * \param ...UNASSIGNED_LOGID
 *      Log arguments associated with the printf-like string.
 */
#define NANO_LOG(severity, format, ...) do { \
    constexpr int numNibbles = NanoLogInternal::getNumNibblesNeeded(format); \
    constexpr int nParams = NanoLogInternal::countFmtParams(format); \
    \
    /*** Very Important*** These must be 'static' so that we can save pointers
     * to these variables and have them persist beyond the invocation.
     * The static logId is used to forever associate this local scope (tied
     * to an expansion of #NANO_LOG) with an id and the paramTypes array is
     * used by the compression function, which is invoked in another thread
     * at a much later time. */ \
    static constexpr std::array<NanoLogInternal::ParamType, nParams> paramTypes = \
                                NanoLogInternal::analyzeFormatString<nParams>(format); \
    static int logId = NanoLogInternal::UNASSIGNED_LOGID; \
    \
    if (NanoLog::severity > NanoLog::getLogLevel()) \
        break; \
    \
    /* Triggers the GNU printf checker by passing it into a no-op function.
     * Trick: This call is surrounded by an if false so that the VA_ARGS don't
     * evaluate for cases like '++i'.*/ \
    if (false) { NanoLogInternal::checkFormat(format, ##__VA_ARGS__); } /*NOLINT(cppcoreguidelines-pro-type-vararg, hicpp-vararg)*/\
    \
    NanoLogInternal::log(logId, __FILE__, __LINE__, NanoLog::severity, format, \
                            numNibbles, paramTypes, ##__VA_ARGS__); \
} while(0)

compiling time的语句

getNumNibblesNeeded:得到prompt中,number的数量

类似%d,%ld,%7.2f这类的。
constexpr int numNibbles = NanoLogInternal::getNumNibblesNeeded(format);

countFmtParams:得到所有的参数的个数

constexpr int nParams = NanoLogInternal::countFmtParams(format);

analyzeFormatString:制作参数队列

static constexpr std::array<NanoLogInternal::ParamType, nParams> paramTypes = 
                            NanoLogInternal::analyzeFormatString<nParams>(format)

这句话中,NanoLogInternal::ParamType, 是模板参数,它本身是一个枚举。所以,类似com编程中的variant类型,为所有的不同的类型,提供一个相同的锚点。
nParams,这个呢是重点。
这里被用到了至少两次,后面在NanoLogInternal::log那里,又被用到。
因为static constexpr std::array相当于定义了静态的数组。要记住,在编译阶段,一切都是静态的,这到什么时候,C++体系也是这样。
多态的VTable虽然是在runtime赋值,但其空间分配,则在编译时就定好的。
因为是静态数组,所以,一切都需要明确。
所以,nParams这里被第一次用到:告知这个数据有多大。
第二次用到是这里,注意,nParams在这里只是一个模板参数,编译器在这里要的不是它的值,而是它的类型:
NanoLogInternal::analyzeFormatString(format)
在后面,一会我们看到,NanoLogInternal::log的调用中,nParams再一次作为类型被这个模板函数所引用。

NanoLogInternal::checkFormat 暂时略过。

if (false) { NanoLogInternal::checkFormat(format, ##VA_ARGS); }
这次我学习nanolog,一方面,是为了将之转化为ctf格式,另外两个方面,一是思考如何进一步优化,例如全面内存化;二是我们在使用中,确有错误发生。

这个错误,显然是某个程序员写的某句NANO_LOG导致,但这句话,pass过了编译器,也pass的log这个数数,直到准备将之解压为人类可读的动作时,才失败。目前我还没有时间去定位。但这也是目标之一。
这么来看,这个check,似乎也没有尽职尽责。

这一段注释的意思:

    /*** Very Important*** These must be 'static' so that we can save pointers
     * to these variables and have them persist beyond the invocation.
     * The static logId is used to forever associate this local scope (tied
     * to an expansion of #NANO_LOG) with an id and the paramTypes array is
     * used by the compression function, which is invoked in another thread
     * at a much later time. */ \
    static constexpr std::array<NanoLogInternal::ParamType, nParams> paramTypes = \
                                NanoLogInternal::analyzeFormatString<nParams>(format); 
    static int logId = NanoLogInternal::UNASSIGNED_LOGID; \

这一段代码,用了两个前置的保留字来修饰:static constexpr
意思是上下文相关的compiling time代码展开。
例如,这一句,作者的意图是在编译时,展开NANO_LOG时,预留一个全局变量,在运行时,将之赋值,而且是唯一的值。
static int logId = NanoLogInternal::UNASSIGNED_LOGID;
注意,logId后面被以引用,或者传址的方式,传给了log函数,由该函数,为该变量在第一次运行时赋永久值。
log(int &logId,
再强调logId是注册号,一旦注册,在程序运行阶段不会再改变。是类型序号,是唯一的,不是log序号。

NanoLogInternal::log

这句显然是重点。我们详细解读之。

NanoLogInternal::log 的定义

这个主函数,我们看到内容并不多


/**
 * Logs a log message in the NanoLog system given all the static and dynamic
 * information associated with the log message. This function is meant to work
 * in conjunction with the #define-d NANO_LOG() and expects the caller to
 * maintain a permanent mapping of logId to static information once it's
 * assigned by this function.
 *
 * \tparam N
 *      length of the format string (automatically deduced)
 * \tparam M
 *      length of the paramTypes array (automatically deduced)
 * \tparam Ts
 *      Types of the arguments passed in for the log (automatically deduced)
 *
 * \param logId[in/out]
 *      LogId that should be permanently associated with the static information.
 *      An input value of -1 indicates that NanoLog should persist the static
 *      log information and assign a new, globally unique identifier.
 * \param filename
 *      Name of the file containing the log invocation
 * \param linenum
 *      Line number within filename of the log invocation.
 * \param severity
 *      LogLevel severity of the log invocation
 * \param format
 *      Static printf format string associated with the log invocation
 * \param numNibbles
 *      Number of nibbles needed to store all the arguments (derived from
 *      the format string).
 * \param paramTypes
 *      An array indicating the type of the n-th format parameter associated
 *      with the format string to be processed.
 *      *** THIS VARIABLE MUST HAVE A STATIC LIFETIME AS PTRS WILL BE SAVED ***
 * \param args
 *      Argument pack for all the arguments for the log invocation
 */
template<long unsigned int N, int M, typename... Ts>
inline void
log(int &logId,
    const char *filename,
    const int linenum,
    const LogLevel severity,
    const char (&format)[M],
    const int numNibbles,
    const std::array<ParamType, N>& paramTypes,
    Ts... args)
{
    using namespace NanoLogInternal::Log;
    assert(N == static_cast<uint32_t>(sizeof...(Ts)));

    if (logId == UNASSIGNED_LOGID) {
        const ParamType *array = paramTypes.data();
        StaticLogInfo info(&compress<Ts...>,
                        filename,
                        linenum,
                        severity,
                        format,
                        sizeof...(Ts),
                        numNibbles,
                        array);

        RuntimeLogger::registerInvocationSite(info, logId);
    }

    uint64_t previousPrecision = -1;
    uint64_t timestamp = PerfUtils::Cycles::rdtsc();
    size_t stringSizes[N + 1] = {}; //HACK: Zero length arrays are not allowed
    size_t allocSize = getArgSizes(paramTypes, previousPrecision,
                            stringSizes, args...) + sizeof(UncompressedEntry);

    char *writePos = NanoLogInternal::RuntimeLogger::reserveAlloc(allocSize);
    auto originalWritePos = writePos;

    UncompressedEntry *ue = new(writePos) UncompressedEntry();
    writePos += sizeof(UncompressedEntry);

    store_arguments(paramTypes, stringSizes, &writePos, args...);

    ue->fmtId = logId;
    ue->timestamp = timestamp;
    ue->entrySize = downCast<uint32_t>(allocSize);

#ifdef ENABLE_DEBUG_PRINTING
    printf("\r\nRecording %d:'%s' of size %u\r\n",
                        logId, info.formatString, ue->entrySize);
#endif

    assert(allocSize == downCast<uint32_t>((writePos - originalWritePos)));
    NanoLogInternal::RuntimeLogger::finishAlloc(allocSize);
}

该函数,我们看到内容并不多。

函数的模板参数

模板参数N

这个容易理解,因为上面我们解释过了。就是上面解释的nParams。
这是它第三次再次被用到。
模板元编程,要理清,代码中用的是它的值,还是类型,这第三是引用它的类型。
template<long unsigned int N, int M, typename… Ts>
这句话,我并没有完全看懂,毕竟有几年没用C++了。
这里是预期的类型。反正我是觉得有点怪,模板的作用主要是类型的泛型,这里又指定,其实有点难以理解,但是这个在C++中叫预期类型,其实你也可以叫它脱了裤子放屁,无非是想编译器帮你报错(这个错,可能让它的设计整体上陷入自己怀疑)。
反正,你要信我,相认这里编译器并不是从模型预定的类型来计算大小,而是根据nParams的类型。
这就是这句代码的解释:
const std::array<ParamType, N>& paramTypes

个人的看法是,之所以写得这么复杂,是因为我们现在的编译器,目前也只能到这个水平。
因为这个参数,是在编译时就全面完成了初始化工作。所以,我们用了泛型,但却是为了能编译过。也许人类未来的编译器,将更加强大,写出更让人容易理解的代码。

再啰嗦一句,在最初没有C++17时,google就开发了nanolog,所以,相对旧一点的代码,是允许不用C++17的,所以,C++17的这些新特性,对于google团队来说,也是新的知识。所以,现在我看这些代码,也不那么觉得奇怪写得有时不那么让人能直观地理解。
之前google是利用编译前的预处理来实现的。这个我不清楚,可能是相当于自己写了编译器的插件,编译之后,进行了处理。

模板参数M

这个,确实第一时间,我不敢肯定,现在基本肯定,确实是编译器会根据用户(业务程序员)编写代码,自动计算出可变参数的数量。
M:表示paramTypes数组的长度,同样由编译器自动推断。这个参数与N的作用相似,它确保了数组的大小与格式字符串中的参数数量相匹配。

模板参数Ts

Ts…:这是一个模板参数包,它包含了传递给log函数的所有参数的类型。这个参数包用于在编译时进行类型检查和类型相关的操作,例如计算参数的数量(sizeof…(Ts))以及在运行时存储参数的实际值。
好吧,这一项我去查了查。并没有完全理解。
…这样的可变参数,我印象中,C语言的解释是双指针数组,似乎。
这样也就能理解为什么sizeof不会错。
即为什么函数第二行的断言能通过的原因:
assert(N == static_cast<uint32_t>(sizeof…(Ts)));
我是这么猜的。

StaticLogInfo info

这一段,是精华中的精华。后现我打算专门在一篇中来写。
它的作用是注册。
具体作用,如果看过CTF,基实这段,与ctf的metadata,或者schema,或者在TMN中被称为MIB(主信息库),是一样的。
所不同的是,一般的schema是,用户手工定义,并且在系统启动前,喂给系统的;
这里的是动态制作出来的。


    if (logId == UNASSIGNED_LOGID) {
        const ParamType *array = paramTypes.data();
        StaticLogInfo info(&compress<Ts...>,
                        filename,
                        linenum,
                        severity,
                        format,
                        sizeof...(Ts),
                        numNibbles,
                        array);

        RuntimeLogger::registerInvocationSite(info, logId);
    }

然后它被注册。
之后是分配内存。
这些内存是在当前的线程的客户端的私有块中分配。

;