首页 > 其他分享 >引擎之旅 Chapter.4 日志系统

引擎之旅 Chapter.4 日志系统

时间:2022-10-05 11:00:36浏览次数:78  
标签:之旅 format PCSTR void Chapter.4 char 堆栈 日志

关于近段时间为何没有更新的解释:Find a new job.

目录

引言

一般来说,一个优质的商业级别的游戏本质上就是一个复杂庞大的软件系统。在庞大系统的开发过程中难免会出现错误。为了排查错误、校验代码的正确性,游戏引擎一般会提供一些调试和开发工具,一般有如下几个:

  • 日志及代码追踪:日志系统一般提供向控制台等页面打印字符串的功能;在打印中也能够清晰的显示调用的堆栈信息,以便于定位代码错误的位置。
  • 调试绘图功能:引擎会提供在游戏场景中绘制辅助线的功能,这些辅助线能清晰的表示范围、方向等信息以供游戏开发者进行调试。
  • 内置菜单:游戏编辑器的一些全局设置,通过不同的设置,方便游戏开发者对特定渲染、逻辑等进行调试。
  • 内置控制台:对于游戏引擎来说,项目为非控制台程序,因此我们无法用简单的使用printf方法将日志输出至控制台。内置控制台就是游戏编辑器中收集和显示日志的窗体。
  • 性能剖析与统计:方便游戏开发者定位性能瓶颈(一个重要的模块)

当然,仅仅这一章节无法去完成对这些调试工具的阐述。本文中的日志系统主要实现了日志及代码堆栈信息的输出功能(上述的第一点),其他部分的内容后续在将其慢慢的完善。本章中的日志系统主要实现一下几点功能:

  • 日志语句可分类,且不同的分类有相关颜色的提示。
  • 日志可打印到控制台窗体、Vistual Studio输出框。
  • 日志可存储至特定的日志文件中。
  • 日志语句可展示相关的堆栈信息。

显示效果如下:

  • 不显示相关堆栈信息
  • 显示相关堆栈信息

日志语句的分类

将日志语句分类可以让开发者打印不同重要性的Log。比如Unity编辑器中的Console将日志语句分为了:Log、Warn、Error三个部分。在TurboEngine的设计中,我将日志分类写为一个枚举类,并将不同的类型在二进制不同的位中岔开,方便筛选。

//日志语句重要性等级
enum LogImportantLevel : int
{
	CodeTrace = 0b00001,   //最低级,用于记录代码执行轨迹(白)
	Info = 0b00010,        //常规,显示日志消息(绿)
	Warn = 0b00100,        //较高级,用于日志警告信息(警告)
	Error = 0b01000,       //高级,用于日志错误信息(错误)
	Critiacal = 0b10000,   //最高级,用于关键日志信息(关键信息)
};

控制台窗体 和 VSOutput Tab的日志打印

这一部分很简单。将日志打印到Console和VS Output主要使用以下两个函数

//to Console
printf(const char* format,...);

//to VS Output
OutputDebugStringA(const char* lpOutputString);

我一般喜欢将特定的功能封装在自己的函数中,一方面可以作为将函数用自己的命名形式统一命名方便调用。另一方面,我们需要对原生函数进行功能上的拓展。OutputDebugStringA 是一个打印字符串的函数,我们要将其封装为OutputDebugStringA(const char* format,...)的形式。

//In TEString.h
//VS函数,将字符串打印到Visual Studio 输出台(分宽字符和常规字符版本)
//--------------------------------------------------------------------------------------------------
inline void TVSOutputDebugString(PCWSTR format, ...)
{
	char* pArgs = (char*)format + sizeof(format);
	_vstprintf_s(TurboCore::GetCommonStrBufferW(), TurboCore::CommonStringBufferSize, format, pArgs);
	::OutputDebugString(TurboCore::GetCommonStrBufferW());
}

inline void TVSOutputDebugString(PCSTR format, ...)
{
	char* pArgs = (char*)format + sizeof(format);
	vsnprintf(TurboCore::GetCommonStrBuffer(), TurboCore::CommonStringBufferSize, format, pArgs);
	::OutputDebugStringA(TurboCore::GetCommonStrBuffer());
}

//对printf()函数的重命名
//--------------------------------------------------------------------------------------------------
inline void TConsoleDebugString(PCSTR format, ...)
{
	char* pArgs = (char*)format + sizeof(format);
	printf(format, pArgs);
}

vsnprintf(char* buffer,size_t bufferSize,const char* format,...) :用于将变量格式化为字符串。

存储至特定的文件中

在Chapter3的文件系统中,我们利用了C语言的文件流函数封装了文件的读写功能。在日志中,我们要利用这一个封装类将日志写入文件中。

相关链接:引擎之旅 Chapter.3 文件系统

实现的思路如下:

  • 在日志类的构造函数中打开一个文件(若没有相关的文件夹则需要创建相关的文件夹)
  • 当调用日志打印时,需要同时将字符串写入文件流中。
  • 在析构函数中将文件关闭
class TURBO_CORE_API TLogger
{
    //日志模式:
    enum class LogFileMode
    {
    	DiskFile,        //日志将存储在磁盘中
    	TempFile         //日志将以临时文件的形式存储(不常用)
    };
    
    TLogger(PCSTR loggerName, LoggerBuffer::BufferSize bufferSize = LOGGER_BUFFER_DEFAULT_SIZE, int logLevelFilter = 0b11111);
    TLogger(PCSTR loggerName,PCSTR logFileSavePath,LoggerBuffer::BufferSize bufferSizeLOGGER_BUFFER_DEFAULT_SIZE,int logLevelFilter = 0b11111);
    
    //注:日志文件不应该支持拷贝函数
    TLogger(const TLogger& clone) = delete;
    ~TLogger();
    
    //输入日志到各个平台:(Console、VSOutputTab、文件流)
    inline void InputLogToAll(PCSTR str);
    inline void InputLogToAll(CHAR c);
}


//Implement
TurboEngine::Core::TLogger::TLogger(PCSTR loggerName, PCSTR logFileSavePath, LoggerBuffer::BufferSize bufferSize, int logLevelFilter)
	:m_LogFileMode(LogFileMode::DiskFile),
	 m_LogBuffer(bufferSize),
	 m_LogLevelFilter(logLevelFilter),
	 m_IsShowCallstack(true)
{
    CHAR dirPath[MAX_PATH_LEN];
    //从文件路径中获取文件所在的文件夹
    TAssert(TPath::GetDirectoryName(dirPath, MAX_PATH_LEN, logFileSavePath));
    
    //判断文件夹目录是否存在,若不存在则创建
    if (!TDirectory::Exists(dirPath))
    	TDirectory::CreateDir(dirPath);
    
    //打开文件流
    TAssert(m_LogFile.Open(logFileSavePath, TFile::FileMode::Text, TFile::FileAccess::ReadWrite_CreateAndClean));
    
    //记录日志的名称和文件路径
    TStrCpy(m_LoggerName, LOGGER_NAME_MAX_LENGTH, loggerName);
    TStrCpy(m_LoggerPath, MAX_PATH_LEN, logFileSavePath);
}

//输入字符串
inline void TurboEngine::Core::TLogger::InputLogToAll(PCSTR str)
{
    m_LogFile.PutStringtLine(str);
    TConsoleDebugString(str);
    TVSOutputDebugString(str);
}

//输入字符
inline void TurboEngine::Core::TLogger::InputLogToAll(CHAR c)
{
    m_LogFile.PutChar(c);
    TConsoleDebugString(&c);
    TVSOutputDebugString(&c);
}

展示堆栈信息

我觉得这是一个可以单独作为一个章节进行阐述,但是日志系统确实也涉及了这一部分的功能,因此,我把也把它写入到本章节中。堆栈信息在游戏或游戏引擎开发是一个十分重要的信息,这个信息可以清晰的展现了当前你打印的这一部分的具体函数调用路径。
关于如何获取到堆栈信息,之后有时间我可以另起一章对这一部分内容进行分析。基本的类结构如下所示:

class TURBO_CORE_API TStackWalker
{
public:
	TStackWalker();
	TStackWalker(DWORD threadId);
	TStackWalker(DWORD threadId, PCSTR symPath);
	~TStackWalker();
}

public:
	inline bool IsInitialized();

    //获取堆栈调用入口数组
    bool GetStackFrameEntryAddressAddrArray(DWORD64 outFrameEntryAddress[STACK_MAX_RECORD]);

    //获取堆栈信息字符串
    void GetCallstackFramesString(PSTR output, size_t outputBufLen, int getNum, int offset);

    //打印堆栈调用信息
    void PrintCallstackFramesLog(DWORD64 frames[STACK_MAX_RECORD]);

    //打印单个栈帧信息
    void PrintSingleCallbackFrameMessage(const CallstackEntry& entry, bool bShowInCosole = false);

protected:
    static BOOL _stdcall MyReadProcMem(HANDLE hProcess, DWORD64 qwBaseAddress, PVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
    //初始化入口
    void Init();
    //获取和初始化符号
    bool InitSymbols();
    //加载所以模块
    bool LoadModules();

    //初始化单个路径的符号
    bool InitSymbol(PCSTR symPath);
    //加载单个模块
    DWORD LoadModule(HANDLE hProcess, LPCSTR img, LPCSTR mod, DWORD64 baseAddr, DWORD size);

关于如何实现,具体可去网上搜索关键字 StackWalker

标签:之旅,format,PCSTR,void,Chapter.4,char,堆栈,日志
From: https://www.cnblogs.com/ZhuSenlin/p/16753507.html

相关文章

  • 基于Kafka+ELK搭建海量日志平台
    早在传统的单体应用时代,查看日志大都通过SSH客户端登服务器去看,使用较多的命令就是less或者tail。如果服务部署了好几台,就要分别登录到这几台机器上看,等到了分布式和微服......
  • R语言观察日志(part26)--去除重复行
    学习笔记,仅供参考,有错必纠准备df<-data.frame(grade=c("A","A","A","B","B","C"),a=c(1:6),b=c(6:1),......
  • 我的NVIDIA开发者之旅——优化显卡性能
    在我们购买任何一样东西时都是更具需求而决定的,买电脑也是,有时是为了办公,就买轻薄本;有些是为了玩游戏,就买游戏本。不同的需求会导致我们购买电脑的硬件有所不同,玩游戏最看重......
  • 0889-7.1.7-Hive on Tez解析以及日志分析
    1.Tez简介Tez是支持DAG作业的开源计算框架,它可以将多个有依赖的作业转换为一个作业从而大幅提升DAG作业的性能。从本质上讲,Tez组成非常简单,只有两个组成部分:数据处理......
  • 如何指定Hadoop命令行日志输出级别
    温馨提示:如果使用电脑查看图片不清晰,可以使用手机打开文章单击文中的图片放大查看高清原图。Fayson的github:​​https://github.com/fayson/cdhproject​​提示:代码块部分可......
  • 如何在NET 6.0使用结构化的日志系统
             在我们的系统里面,有一项技术是必须使用的,那就是日志记录。我们在调试系统或者跟踪系统运行情况,都可以通过日志了解具体的情况。在项目开发中,我们有可......
  • SQL Server 日志清理SQL
    USE[master]GOALTERDATABASE要清理的数据库名称SETRECOVERYSIMPLEWITHNO_WAITGOALTERDATABASE要清理的数据库名称SETRECOVERYSIMPLE-......
  • 我的编程之旅
    我是一名程序员,虽然码龄只有两年,但是读过许多经典的书籍,比如《java核心技术卷I》,《java核心技术卷II》,学了许多框架,从最初的javaweb到ssm,再到springboot,以及springclo......
  • Spring自学日志01-IOC(控制翻转)
    目录一、IOC的基本概念和底层原理1.1、什么是IOC?1.1.1、SpringIOC容器1.2、IOC底层原理1.2.1、IOC容器1.2.2、IOC容器装配Bean的方式1.2.3、IOC容器装配Bean的操作1.2.3.......
  • linux_文件跟踪查看/实时查看添加到⽂件中的内容/linux日志查看(outline)
    文章目录​​文件跟踪查看/实时查看添加到⽂件中的内容/linux日志查看​​​​追踪更新中的文件​​​​查看某个文件的末尾N行,并带上源文件中的行号​​​​查看linux内核......