Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul of the Logging Infrastructure #1596

Open
wants to merge 60 commits into
base: dev
Choose a base branch
from

Conversation

Dimi1010
Copy link
Collaborator

@Dimi1010 Dimi1010 commented Sep 30, 2024

Overhaul of the Logging Infrastructure

Changes

  • Breaking changes: LogLevel has been updated to an enum class and moved to the pcpp namespace.
    • Logger retains aliases to the new enum.
    • Impact: May cause issues for users performing arithmetic operations on LogLevel, but this is not expected to be a major concern.
  • Logging Improvements:
    • Fixed incorrect or missing LogModule values in the LoggingModule enum.
    • Introduces helper classes to organize logging data:
      • LogSource: Encapsulates file, function, line number and module information.
      • LogContext: Encapsulates the context of a single log message.
  • API Enhancements:
    • Formalized internal methods of Logger class into public methods with documentation.
      • Replaced internalCreateLogStream with createLogContext.
      • Replaced internalPrintLogMessage with emit.
    • Added helper log* functions to Logger class to allow logging, through runtime methods.
    • Standardized all PCPP_LOG_* macros to pass through PCPP_LOG.
  • Performance Optimizations:
    • Introduced an optional object pooling mechanism for LogContext to reduce allocation overhead.
    • Added a generic ObjectPool<T> class to facilitate pooling for reusable objects.
    • The optimization is enabled by default and can be disabled by calling useContextPooling(false) on the Logger instance.
  • Compile-Time Logging Exclusions:
    • Added support for excluding log messages defined through PCPP_LOG macros at compile time via PCPP_ACTIVE_LOG_LEVEL preprocessor variable.
  • Code Quality Enhancements:
    • Added std::mutex locking in defaultLogPrinter to eliminate possible data races during log message emission.
    • Moved ostream& operator<< overloads for IPAddress and MacAddress inside pcpp to satisfy ADL (Argument-Dependent Lookup).

LogContext Pooling Optimization

When enabled, the pooling optimization introduces the following behavior:

  • Allocation on createLogContext:

    • When createLogContext is called, the pool is queried for an available LogContext.
    • If no available LogContext is found, a new one is created immediately.
  • Releasing on emit:

    • When the emit method with a unique_ptr<LogContext> parameter is called, the LogContext is returned to the pool after the operation completes.
    • If the pool has not reached its maximum retention size, the LogContext is stored for reuse.
    • If the pool exceeds its maximum retention size, the LogContext is deleted.

Optimization Benchmarks

Performance benchmarks highlight the impact of enabling the pooling optimization:

Windows 10 - MSVC x64 (Release Build)

  • Warmup Messages: 5,000
  • Benchmarked Messages: 300,000
  • Results:
    • Pooling Enabled: 104,387,800 ns
    • Pooling Disabled: 179,670,900 ns

WSL (Ubuntu 22.04) - GCC x64 (Release Build)

  • Warmup Messages: 5,000
  • Benchmarked Messages: 300,000
  • Results:
    • Pooling Enabled: 64,135,976 ns
    • Pooling Disabled: 125,265,749 ns

Notes

  • The pooling optimization offers significant performance gains by reducing the cost of frequent allocations and deallocations.
  • Benchmarks suggest up to ~42% improvement on Windows and ~48% improvement on Linux.
  • While the changes to LogLevel and other enums may introduce minor compatibility issues, they align with modern C++ best practices.

- LogLevel is now a top level enum class.
- Added a new log level Off to disable output from a specific module.
- Logger::LogLevel is a deprecated alias to LogLevel.
- Logger::Info, Debug, Error are deprecated aliases to LogLevel::...
- Removed public "internal*" functions from Logger.Logger
- Added LogSource struct to encapsulate source information.
- Added shouldLog method to check if a log should be emitted for a given level and module.
- Removed nonfunctional artifacts "m_LogStream" and "Logger::operator<<"
- Added templated "log" functions that are friends to Logger.
- Reworked PCPP_LOG macros to no longer utilize the now removed internal functions.
- Added PCPP_LOG_INFO macro level.
- Changed PCPP_LOG_ERROR to now check if the log should be emitted.
- Fixed NetworkUtils log module name overlapping with NetworkUtils class.
- Fixed missing enum value for PacketLogModuleSll2Layer.
@Dimi1010 Dimi1010 force-pushed the refactor/log-templates branch from bd53c74 to f21f85d Compare September 30, 2024 15:09
Copy link

codecov bot commented Sep 30, 2024

Codecov Report

Attention: Patch coverage is 91.63180% with 20 lines in your changes missing coverage. Please review.

Project coverage is 83.16%. Comparing base (7898a5d) to head (117ff49).

Files with missing lines Patch % Lines
Common++/header/Logger.h 79.16% 10 Missing ⚠️
Common++/src/Logger.cpp 76.92% 8 Missing and 1 partial ⚠️
Pcap++/src/PcapLiveDeviceList.cpp 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##              dev    #1596    +/-   ##
========================================
  Coverage   83.16%   83.16%            
========================================
  Files         277      279     +2     
  Lines       48201    48403   +202     
  Branches     9932    10284   +352     
========================================
+ Hits        40086    40255   +169     
+ Misses       7266     7041   -225     
- Partials      849     1107   +258     
Flag Coverage Δ
alpine320 75.15% <86.71%> (+<0.01%) ⬆️
fedora40 75.19% <86.04%> (+0.02%) ⬆️
macos-13 80.64% <83.09%> (-0.02%) ⬇️
macos-14 80.64% <83.09%> (-0.02%) ⬇️
macos-15 80.61% <83.01%> (-0.02%) ⬇️
mingw32 70.85% <74.03%> (-0.07%) ⬇️
mingw64 70.80% <74.03%> (-0.09%) ⬇️
npcap 85.26% <81.81%> (-0.06%) ⬇️
rhel94 75.03% <86.82%> (-0.01%) ⬇️
ubuntu2004 58.63% <80.50%> (+<0.01%) ⬆️
ubuntu2004-zstd 58.75% <80.50%> (+0.02%) ⬆️
ubuntu2204 74.97% <86.82%> (-0.01%) ⬇️
ubuntu2204-icpx 61.21% <66.42%> (-0.24%) ⬇️
ubuntu2404 75.22% <86.71%> (+<0.01%) ⬆️
unittest 83.16% <91.63%> (+<0.01%) ⬆️
windows-2019 85.29% <81.81%> (-0.06%) ⬇️
windows-2022 85.32% <81.67%> (-0.06%) ⬇️
winpcap 85.29% <81.81%> (-0.05%) ⬇️
xdp 50.40% <23.23%> (-0.13%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Dimi1010 Dimi1010 marked this pull request as ready for review October 7, 2024 09:50
@Dimi1010 Dimi1010 requested a review from seladb as a code owner October 7, 2024 09:50
@Dimi1010 Dimi1010 added breaking change Pull request contains a breaking change to the public API. refactoring labels Oct 7, 2024
Comment on lines 387 to 388
std::ostringstream sstream; \
sstream << message; \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please check if this change affects the PcapPlusPlus binary sizes?

I made a change in this PR a couple of years ago to significantly reduce the binary sizes: #748

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breakdown on the install directory for the x64 release w/npcap.

Folder1 is the new size. Folder2 is the old size.

File                                                     Folder1Size Folder2Size AbsoluteDelta RelativeDelta
----                                                     ----------- ----------- ------------- -------------
bin\Arping.exe                                                714240      639488         74752 10.47%
bin\ArpSpoofing.exe                                           703488      631296         72192 10.26%
bin\benchmark.exe                                             599552      562688         36864 6.15%
bin\DisplayLiveDevices.exe                                     30208       30208             0 0.00%
bin\DNSResolver.exe                                           748032      664064         83968 11.23%
bin\DnsSpoofing.exe                                           747520      671232         76288 10.21%
bin\HttpAnalyzer.exe                                          832512      743936         88576 10.64%
bin\IcmpFileTransfer-catcher.exe                              730624      658432         72192 9.88%
bin\IcmpFileTransfer-pitcher.exe                              742400      666112         76288 10.28%
bin\IPDefragUtil.exe                                          711680      655360         56320 7.91%
bin\IPFragUtil.exe                                            693248      637952         55296 7.98%
bin\PcapPrinter.exe                                           658432      612352         46080 7.00%
bin\PcapSearch.exe                                            661504      618496         43008 6.50%
bin\PcapSplitter.exe                                          720384      667648         52736 7.32%
bin\SSLAnalyzer.exe                                           822272      733184         89088 10.83%
bin\TcpReassembly.exe                                         799232      710144         89088 11.15%
bin\TLSFingerprinting.exe                                     796672      712704         83968 10.54%
lib\Common++.lib                                             5045102     4887832        157270 3.12%
lib\Packet++.lib                                            19849740    18454316       1395424 7.03%
lib\Pcap++.lib                                               3620658     2896688        723970 20.00%

Full breakdown: npcap-x64-release.txt

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test it with MSVC or MinGW? Also, did you have a chance to test on Linux also?

I don't remember in which platform the impact was the highest, but I think it was Linux 🤔

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Msvc, I dont think I have mingw setup. I should be able to test the sizes on Unix via WSL.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Dimi1010 I see you're still working on it. Please let me know when the changes are done and when you want me to review the PR again.
It'd be good to make sure binary sizes don't increase. I'd test at least on Windows and Linux

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seladb Ok, ended up keeping the optimization.

As bc82caf: Release builds ended up being:

  • Windows
    • Static Libraries
      • Common++.lib - 4.7MB -> 5MB
      • Packet++.lib - 17.6MB -> 19.7MB
      • Pcap++.lib - 2.8MB -> 3.3MB
    • Examples
      • Same binary size or <=10KB increase.
  • Linux (WSL)
    • Static Libraries
      • libCommon++.a - 408KB -> 444KB
      • libPakcet++.a - 3.1MB -> 3.3MB
      • libPcap++.a - 676KB -> 748KB
    • Examples
      • On average ~0.1MB increase on each binary.

@Dimi1010 Dimi1010 marked this pull request as draft December 25, 2024 19:01
@Dimi1010 Dimi1010 force-pushed the refactor/log-templates branch from d9df72b to 421a419 Compare January 3, 2025 15:13
@Dimi1010 Dimi1010 marked this pull request as ready for review January 5, 2025 09:56
@Dimi1010 Dimi1010 requested a review from seladb January 5, 2025 09:59
Examples/PfRingExample-FilterTraffic/main.cpp Show resolved Hide resolved
Examples/KniPong/main.cpp Show resolved Hide resolved
{
// We don't need the lock anymore, so release it.
lock.unlock();
return new T();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the pool is empty it allows acquiring any number of objects. Meaning, if the pool is initialized with preallocate = 0 which is the default, the user can call acquireObject() an infinite number of times before they call releaseObject(). This is a bit confusing assuming the pool has a pre-configured size (maxPoolSize)

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pool has a max size to the objects it will store. Not the objects it will generate. The user should not concern himself if the pool is empty before asking for a new object because the pool will always provide him an object. Whether the object is brand new or reused is an implementation detail of the pool.

PS: That was the reason why originally there wasn't a way to check the current size of the pool.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat counter-intuitive because pools are usually used to pre-allocate objects and offer them to the user instead of just-in-time memory allocation. This pool is unique because it offers pre-allocated objects if it has any, otherwise it allocates new one, and the user doesn't know / cannot control whether memory is allocated or not 🤔

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it can seem so. Although I have seen many pools that do just-in-time allocations when needed. For example EFCore's DbContextPool implementation.

The options for a pool when a new entity is required and it is empty are three:

  1. Return an error.
  2. JIT a new instance of the object.
  3. Wait for another thread to release an object.

I preferred option 2, because the pool designed for a drop in replacement for somewhere where the objects would already be JIT-ed. It just provides optimization for reuse if it can, and just works if it can't.

When we don't want to cancel the operation Option 1 is just Option 2 or Option 3 with extra steps because the user has to do the checks manually at every call site. There is also the case for release, to mirror the acquire function, it should also return an error when the pool is already full, which is again Option 2 with extra steps where the object needs to be manually released.

Option 3 isn't desirable because it would essentially block the operation for an unknown amount of time.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In object-pools objects are usually pre-allocated so an empty pool essentially means "out of memory", thus Option 2 suggests increasing the total memory beyond what was allocated initially. In our case since pre-allocation is optional we don't know if an empty pool means "out of memory" or not. The ways to solve it are:

  1. Force pre-allocating of all objects when the pool is created
  2. Give the option to avoid pre-allocation, but count the number of objects that were allocated

If we choose one of these options we can also let the user decide what to do when the pool is empty via a flag in the c'tor (return an error or JIT a new instance and increase maxPoolSize).

I agree Option 3 isn't desirable.

Copy link
Collaborator Author

@Dimi1010 Dimi1010 Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they are more common, but dynamic object pools have been used where the the demands can fluctuate between common low number of active objects and occasional spikes, which I believe is the current use case for LogContext.

Demands for LogContext will usually be in the range of 1~3 objects based on the number of threads that are simultaneously using the pcpp logger. There is however the option for the user to spawn many capture threads and the logs to become congested. This is my rationale for using a dynamic pool with 2 initial size and 10 soft cap (maxPoolSize) where objects released to the pool after there are 10 objects in the pool won't be stored, as the likelihood of them being needed again is low. (that would require 10+ threads logging something at the exact same moment)

Also the pool is technically not supposed to be a user facing class but an implementation detail made for the logger, so I think it might be beneficial to shift it to the internals namespace.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you already implemented an object pool we can consider using it in other places as well (although I can't think of a specific example right now). If you have a strong opinion about making it a dynamic object pool, maybe we can rename the class to DynamicObjectPool and document what it does, so in the future we can also implement a more "standard" object pool?

I agree we can move it to the internal namespace so it's not exposed to library users


#include "ObjectPool.h"

PTF_TEST_CASE(TestObjectPool)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add another tests that runs clear()

Comment on lines +384 to +386
/// @brief Logs a message with the given source, level, and message.
/// @param message The log message.
void log(std::unique_ptr<internal::LogContext> message);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we use this overload of log() anywhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not atm. I initially thought it might be used in the macros, but they ended up using emit directly as they already checked the log level.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So let's remove it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking change Pull request contains a breaking change to the public API. refactoring
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants