Skip to content

perf(logging): strip debug log messages at compile-time#4365

Merged
andynog merged 7 commits intomainfrom
andy/compile-time-debug-strip
Oct 31, 2024
Merged

perf(logging): strip debug log messages at compile-time#4365
andynog merged 7 commits intomainfrom
andy/compile-time-debug-strip

Conversation

@andynog
Copy link
Collaborator

@andynog andynog commented Oct 29, 2024

partially close: #2847

Logging debug messages can lead to significant memory allocations, especially when outputting variable values. In Go, even if log_level is not set to debug, these allocations can still occur because the program evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level code from the binary at compile time using build flags. This approach improves the performance of CometBFT by excluding debug messages entirely, even when log_level is set to debug. This technique is ideal for production environments that prioritize performance optimization over debug logging.

Note: Compiling CometBFT with this method will completely disable all debug messages. If you require debug output, avoid compiling the binary with these settings.

In order to build a binary stripping all debug log messages (e.g. log.Debug()) from the binary, use the nodebug tag:

COMETBFT_BUILD_OPTIONS=nodebug make install

once it's installed you can run a kvstore example with log_level = "debug" set in the config.toml and you will see that no debug messages are shown in the log.

cc: @ValarDragon

PR checklist

  • Tests written/updated
  • Changelog entry added in .changelog (we use unclog to manage our changelog)
  • Updated relevant documentation (docs/ or spec/) and code comments

@andynog andynog added P:operator-experience Priority: Improve experience for operators logs Anything relating to logging labels Oct 29, 2024
@andynog andynog added this to the 2024-Q4 milestone Oct 29, 2024
@andynog andynog self-assigned this Oct 29, 2024
@andynog andynog requested a review from a team as a code owner October 29, 2024 20:36
@andynog andynog requested a review from a team October 29, 2024 20:36
Copy link
Collaborator

@melekes melekes left a comment

Choose a reason for hiding this comment

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

Thanks @andynog ❤️

@melekes
Copy link
Collaborator

melekes commented Oct 30, 2024

We're still incurring the cost of calculating arguments (if they are not Lazy), correct? Have you done any benchmarks? Curious about the results 🦊

@ValarDragon
Copy link
Contributor

ValarDragon commented Oct 30, 2024

Is doing this at compile time best? feels hard to get operators to re-compile, vs having a global variable being set to off based on reading the config

If its at compile time, were anyways doing something global

@andynog
Copy link
Collaborator Author

andynog commented Oct 30, 2024

We're still incurring the cost of calculating arguments (if they are not Lazy), correct? Have you done any benchmarks? Curious about the results 🦊

I will add more insights about this today before merging.

@andynog
Copy link
Collaborator Author

andynog commented Oct 30, 2024

@melekes and @ValarDragon, to clarify the solution, it leverages a Go feature that removes dead code at compile time. Essentially, this strips out any Debug instructions from the final binary, ensuring zero allocations since these instructions don’t exist in the compiled output.

For example, here's the Debug func in the tm_logger.go file.
image

Note that it is on line 63. If we compile a binary that includes the debug messages (e.g. don't compile with the nodebug tag), you can see from the objdump that these instructions are part of the binary

(cometbft) > go tool objdump ./build/cometbft | grep tm_logger.go:63
  tm_logger.go:63                       0x100278570             f9400b90                MOVD 16(R28), R16                                       
  tm_logger.go:63                       0x100278574             d10083f1                SUB $32, RSP, R17                                       
  tm_logger.go:63                       0x100278578             eb10023f                CMP R16, R17                                            
  tm_logger.go:63                       0x10027857c             54001209                BLS 144(PC)                                             
  tm_logger.go:63                       0x100278580             f8160ffe                MOVD.W R30, -160(RSP)                                   
  tm_logger.go:63                       0x100278584             f81f83fd                MOVD R29, -8(RSP)                                       
  tm_logger.go:63                       0x100278588             d10023fd                SUB $8, RSP, R29                                        
  tm_logger.go:63                       0x1002787bc             f90007e0                MOVD R0, 8(RSP)                                         
  tm_logger.go:63                       0x1002787c0             f9000be1                MOVD R1, 16(RSP)                                        
  tm_logger.go:63                       0x1002787c4             f9000fe2                MOVD R2, 24(RSP)                                        
  tm_logger.go:63                       0x1002787c8             f90013e3                MOVD R3, 32(RSP)                                        
  tm_logger.go:63                       0x1002787cc             f90017e4                MOVD R4, 40(RSP)                                        
  tm_logger.go:63                       0x1002787d0             f9001be5                MOVD R5, 48(RSP)                                        
  tm_logger.go:63                       0x1002787d4             aa1e03e3                MOVD R30, R3                                            
  tm_logger.go:63                       0x1002787d8             97f8157a                CALL -518790(PC)                                        
  tm_logger.go:63                       0x1002787dc             f94007e0                MOVD 8(RSP), R0                                         
  tm_logger.go:63                       0x1002787e0             f9400be1                MOVD 16(RSP), R1                                        
  tm_logger.go:63                       0x1002787e4             f9400fe2                MOVD 24(RSP), R2                                        
  tm_logger.go:63                       0x1002787e8             f94013e3                MOVD 32(RSP), R3                                        
  tm_logger.go:63                       0x1002787ec             f94017e4                MOVD 40(RSP), R4                                        
  tm_logger.go:63                       0x1002787f0             f9401be5                MOVD 48(RSP), R5                                        
  tm_logger.go:63                       0x1002787f4             17ffff5f                JMP -161(PC)                                            
  tm_logger.go:63                       0x1002787f8             00000000                ?                                                       
  tm_logger.go:63                       0x1002787fc             00000000                ?                      

If you compile a binary with the nodebug tag, it will strip out all the instructions for Debug:

(cometbft) > COMETBFT_BUILD_OPTIONS=nodebug make build              
CGO_ENABLED=1 go build -mod=readonly -ldflags "-X github.com/cometbft/cometbft/version.CMTGitCommitHash=980d8a315  -s -w" -trimpath -tags 'cometbft nodebug' -o /Users/andynogueira/Development/Go/src/github.com/cometbft/cometbft/build/cometbft ./cmd/cometbft/
(cometbft) > go tool objdump ./build/cometbft | grep tm_logger.go:63
(cometbft) > 

I recognize that, from an operator's perspective, re-compiling a binary may not initially seem ideal. However, this approach is the safest and most effective way to eliminate any risk of extra allocations from debug messages, ultimately boosting performance for operators. Given these benefits, I believe re-compilation will be a reasonable step for them.

Alternatively, we could explore a solution based on conditional statements (if checks), though this would involve a significant refactor and still might not fully eliminate allocations. If needed, I can add additional tests to validate these outcomes and ensure reliability. Might be a good idea to add benchmarks too.

@melekes
Copy link
Collaborator

melekes commented Oct 30, 2024

compile time is the way 👍

@cason
Copy link

cason commented Oct 31, 2024

The question for me now is whether the official binaries should have debug level enabled or not.

I agree with @ValarDragon that changing the executed binary when we want to debug something is not really the best way to go.

At the same time, this is another (apparently very efficient) option to reduce the cost of debug log calls, when debugging is disabled.

@andynog
Copy link
Collaborator Author

andynog commented Oct 31, 2024

We agreed this is a good use case for operators looking for optimal performance. However, there might be alternatives that would require more refactoring but would allow a granular and configurable approach to the problem. I will merge this and say it partially closes the issue. In the future, I can submit another PR that implements the other approach, too, so this way, we will cater to multiple operators.

@andynog andynog added this pull request to the merge queue Oct 31, 2024
Merged via the queue into main with commit 51e9948 Oct 31, 2024
@andynog andynog deleted the andy/compile-time-debug-strip branch October 31, 2024 13:08
mergify bot added a commit that referenced this pull request Oct 31, 2024
partially close: #2847

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon

#### PR checklist

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 51e9948)
mergify bot added a commit that referenced this pull request Oct 31, 2024
…) (#4375)

partially close: #2847 

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon 

#### PR checklist

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments
<hr>This is an automatic backport of pull request #4365 done by
[Mergify](https://mergify.com).

Co-authored-by: Andy Nogueira <me@andynogueira.dev>
mattac21 pushed a commit that referenced this pull request Sep 9, 2025
partially close: #2847

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
mattac21 pushed a commit that referenced this pull request Sep 10, 2025
partially close: #2847

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
mattac21 pushed a commit that referenced this pull request Sep 10, 2025
partially close: #2847

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
mattac21 pushed a commit that referenced this pull request Sep 10, 2025
partially close: #2847

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
mattac21 pushed a commit that referenced this pull request Sep 10, 2025
partially close: #2847

Logging debug messages can lead to significant memory allocations,
especially when outputting variable values. In Go, even if log_level is
not set to debug, these allocations can still occur because the program
evaluates the debug statements regardless of the log level.

To prevent unnecessary memory usage, you can strip out all debug-level
code from the binary at compile time using build flags. This approach
improves the performance of CometBFT by excluding debug messages
entirely, even when `log_level` is set to `debug`. This technique is
ideal for production environments that prioritize performance
optimization over debug logging.

> Note: Compiling CometBFT with this method will completely disable all
debug messages. If you require debug output, avoid compiling the binary
with these settings.

In order to build a binary stripping all debug log messages (e.g.
`log.Debug()`) from the binary, use the `nodebug` tag:
```
COMETBFT_BUILD_OPTIONS=nodebug make install
```

once it's installed you can run a `kvstore` example with `log_level =
"debug"` set in the `config.toml` and you will see that no debug
messages are shown in the log.

cc: @ValarDragon

- [ ] ~~Tests written/updated~~
- [X] Changelog entry added in `.changelog` (we use
[unclog](https://github.com/informalsystems/unclog) to manage our
changelog)
- [X] Updated relevant documentation (`docs/` or `spec/`) and code
comments

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

logs Anything relating to logging P:operator-experience Priority: Improve experience for operators

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

Don't pay vote Debug logging cost in normal operation

4 participants