Skip to content

Commit d64da2a

Browse files
authored
feat(avm)!: optionally use TS logger in C++ simulation (#19305)
This PR * Makes BB's `info`/`vinfo`/etc customizable by a logging function * Updates the TS side of CPP simulation to optionally take a logger function. The TS-side of the CPP simulator now uses the pino logger. ![image.png](https://app.graphite.com/user-attachments/assets/ca2fbaf1-2a72-4590-a9e9-eeb92563e1d3.png) I've also refactored the `avm_simulate_napi.cpp` file to avoid referencing the parameter indices many times, and using them in a very "local" way. Otherwise it was getting a bit dangerous that we would use some magic index in places many lines away. This shouldn't cause any overhead if logging is not active (as expected in block building, because ANY logging might take a `ms` - e.g., showing the transaction hash). If you are doing any logging, you will pay dearly. Closes [AVM-182](https://linear.app/aztec-labs/issue/AVM-182/optionally-use-ts-logger-in-c-simulation).
1 parent 669c29e commit d64da2a

File tree

12 files changed

+173
-66
lines changed

12 files changed

+173
-66
lines changed

barretenberg/cpp/src/barretenberg/common/log.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include <cstdlib>
33
#include <string>
44

5+
#include "barretenberg/common/log.hpp"
6+
57
#ifndef __wasm__
68
bool verbose_logging = std::getenv("BB_VERBOSE") == nullptr ? false : std::string(std::getenv("BB_VERBOSE")) == "1";
79
#else
@@ -10,3 +12,11 @@ bool verbose_logging = true;
1012

1113
// Used for `debug` in log.hpp.
1214
bool debug_logging = false;
15+
16+
// Used for `log_function` in log.hpp. Defaults to `logstr`.
17+
LogFunction log_function = [](LogLevel /*unused*/, const char* msg) { logstr(msg); };
18+
19+
void set_log_function(LogFunction new_log_function)
20+
{
21+
log_function = std::move(new_log_function);
22+
}

barretenberg/cpp/src/barretenberg/common/log.hpp

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#pragma once
2-
#include "barretenberg/env/logstr.hpp"
3-
#include "barretenberg/stdlib/primitives/circuit_builders/circuit_builders_fwd.hpp"
2+
43
#include <algorithm>
54
#include <functional>
65
#include <sstream>
76
#include <string>
87
#include <vector>
98

9+
#include "barretenberg/env/logstr.hpp"
10+
#include "barretenberg/stdlib/primitives/circuit_builders/circuit_builders_fwd.hpp"
11+
1012
#define BENCHMARK_INFO_PREFIX "##BENCHMARK_INFO_PREFIX##"
1113
#define BENCHMARK_INFO_SEPARATOR "#"
1214
#define BENCHMARK_INFO_SUFFIX "##BENCHMARK_INFO_SUFFIX##"
@@ -32,7 +34,7 @@ template <typename T> void benchmark_format_chain(std::ostream& os, T const& fir
3234
std::stringstream current_argument;
3335
current_argument << first;
3436
std::string current_argument_string = current_argument.str();
35-
std::replace(current_argument_string.begin(), current_argument_string.end(), ',', ';');
37+
std::ranges::replace(current_argument_string, ',', ';');
3638
os << current_argument_string << BENCHMARK_INFO_SUFFIX;
3739
}
3840

@@ -43,7 +45,7 @@ void benchmark_format_chain(std::ostream& os, T const& first, Args const&... arg
4345
std::stringstream current_argument;
4446
current_argument << first;
4547
std::string current_argument_string = current_argument.str();
46-
std::replace(current_argument_string.begin(), current_argument_string.end(), ',', ';');
48+
std::ranges::replace(current_argument_string, ',', ';');
4749
os << current_argument_string << BENCHMARK_INFO_SEPARATOR;
4850
benchmark_format_chain(os, args...);
4951
}
@@ -56,6 +58,18 @@ template <typename... Args> std::string benchmark_format(Args... args)
5658
return os.str();
5759
}
5860

61+
enum class LogLevel : int {
62+
DEBUG = 0,
63+
INFO = 1,
64+
VERBOSE = 2,
65+
IMPORTANT = 3,
66+
};
67+
68+
// This allows the logging sink to be customized. Useful for Typescript use-cases.
69+
using LogFunction = std::function<void(LogLevel level, const char* msg)>;
70+
extern LogFunction log_function;
71+
void set_log_function(LogFunction new_log_function);
72+
5973
extern bool debug_logging;
6074
// In release mode (e.g., NDEBUG is defined), we don't compile debug logs.
6175
#ifndef NDEBUG
@@ -68,13 +82,13 @@ extern bool debug_logging;
6882
inline void debug_(std::function<std::string()> func)
6983
{
7084
if (debug_logging) {
71-
logstr(func().c_str());
85+
log_function(LogLevel::DEBUG, func().c_str());
7286
}
7387
}
7488

7589
template <typename... Args> inline void info(Args... args)
7690
{
77-
logstr(format(args...).c_str());
91+
log_function(LogLevel::INFO, format(args...).c_str());
7892
}
7993

8094
#define vinfo(...) vinfo_([&]() { return format(__VA_ARGS__); })
@@ -83,19 +97,19 @@ extern bool verbose_logging;
8397
inline void vinfo_(std::function<std::string()> func)
8498
{
8599
if (verbose_logging) {
86-
info(func());
100+
log_function(LogLevel::VERBOSE, func().c_str());
87101
}
88102
}
89103

90104
template <typename... Args> inline void important(Args... args)
91105
{
92-
logstr(format("important: ", args...).c_str());
106+
log_function(LogLevel::IMPORTANT, format("important: ", args...).c_str());
93107
}
94108

95109
/**
96110
* @brief Info used to store circuit statistics during CI/CD with concrete structure. Writes straight to log
97111
*
98-
* @details Automatically appends the necessary prefix and suffix, as well as separators.
112+
* @details Automatically appends the necessary prefix and suffix, as well as separators.
99113
*
100114
* @tparam Args
101115
* @param args
@@ -129,7 +143,7 @@ class BenchmarkInfoCollator {
129143
* @brief Info used to store circuit statistics during CI/CD with concrete structure. Stores string in vector for now
130144
* (used to flush all benchmarks at the end of test).
131145
*
132-
* @details Automatically appends the necessary prefix and suffix, as well as separators.
146+
* @details Automatically appends the necessary prefix and suffix, as well as separators.
133147
*
134148
* @tparam Args
135149
* @param args

barretenberg/cpp/src/barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.cpp

Lines changed: 116 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
#include "avm_simulate_napi.hpp"
1+
#include "barretenberg/nodejs_module/avm_simulate/avm_simulate_napi.hpp"
22

3+
#include <array>
34
#include <memory>
45
#include <vector>
56

@@ -13,8 +14,8 @@
1314
#include "barretenberg/vm2/simulation/lib/cancellation_token.hpp"
1415

1516
namespace bb::nodejs {
16-
1717
namespace {
18+
1819
// Log levels from TS foundation/src/log/log-levels.ts: ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug',
1920
// 'trace'] Map: 0=silent, 1=fatal, 2=error, 3=warn, 4=info, 5=verbose, 6=debug, 7=trace
2021
constexpr int LOG_LEVEL_VERBOSE = 5;
@@ -29,6 +30,46 @@ inline void set_logging_from_level(int log_level)
2930
debug_logging = (log_level >= LOG_LEVEL_TRACE);
3031
}
3132

33+
// Map C++ LogLevel enum to TypeScript log level string
34+
// C++ LogLevel: DEBUG=0, INFO=1, VERBOSE=2, IMPORTANT=3
35+
// TS LogLevels: ['silent', 'fatal', 'error', 'warn', 'info', 'verbose', 'debug', 'trace']
36+
inline const char* cpp_log_level_to_ts(LogLevel level)
37+
{
38+
switch (level) {
39+
case LogLevel::DEBUG:
40+
return "debug";
41+
case LogLevel::INFO:
42+
return "info";
43+
case LogLevel::VERBOSE:
44+
return "verbose";
45+
case LogLevel::IMPORTANT:
46+
return "warn";
47+
default:
48+
return "info";
49+
}
50+
}
51+
52+
// Helper to create a LogFunction wrapper from a ThreadSafeFunction
53+
// This allows C++ logging to call back to TypeScript logger from worker threads
54+
LogFunction create_log_function_from_tsfn(const std::shared_ptr<Napi::ThreadSafeFunction>& logger_tsfn)
55+
{
56+
return [logger_tsfn](LogLevel level, const char* msg) {
57+
// Convert C++ LogLevel to TS log level string
58+
const char* ts_level = cpp_log_level_to_ts(level);
59+
std::string msg_str(msg);
60+
61+
// Call TypeScript logger function on the JS main thread
62+
// Using BlockingCall to ensure synchronous execution
63+
// Ignore errors - logging failures shouldn't crash the simulation
64+
logger_tsfn->BlockingCall([ts_level, msg_str](Napi::Env env, Napi::Function js_logger) {
65+
// Create arguments: (level: string, msg: string)
66+
auto level_js = Napi::String::New(env, ts_level);
67+
auto msg_js = Napi::String::New(env, msg_str);
68+
js_logger.Call({ level_js, msg_js });
69+
});
70+
};
71+
}
72+
3273
// Callback method names
3374
constexpr const char* CALLBACK_GET_CONTRACT_INSTANCE = "getContractInstance";
3475
constexpr const char* CALLBACK_GET_CONTRACT_CLASS = "getContractClass";
@@ -117,63 +158,42 @@ Napi::Value AvmSimulateNapi::simulate(const Napi::CallbackInfo& cb_info)
117158
{
118159
Napi::Env env = cb_info.Env();
119160

120-
// Validate arguments - expects 4-5 arguments
161+
// Validate arguments - expects 3-6 arguments
121162
// arg[0]: inputs Buffer (required)
122163
// arg[1]: contractProvider object (required)
123164
// arg[2]: worldStateHandle external (required)
124-
// arg[3]: logLevel number (required) - index into TS LogLevels array
125-
// arg[4]: cancellationToken external (optional)
126-
if (cb_info.Length() < 4) {
127-
throw Napi::TypeError::New(env,
128-
"Wrong number of arguments. Expected 4-5 arguments: inputs Buffer, contractProvider "
129-
"object, worldStateHandle, logLevel, and optional cancellationToken.");
165+
// arg[3]: logLevel number (optional) - index into TS LogLevels array, -1 if omitted
166+
// arg[4]: loggerFunction (optional) - can be null/undefined
167+
// arg[5]: cancellationToken external (optional)
168+
if (cb_info.Length() < 3) {
169+
throw Napi::TypeError::New(
170+
env,
171+
"Wrong number of arguments. Expected 3-6 arguments: inputs Buffer, contractProvider "
172+
"object, worldStateHandle, optional logLevel, optional loggerFunction, and optional cancellationToken.");
130173
}
131174

175+
/*******************************
176+
*** AvmFastSimulationInputs ***
177+
*******************************/
132178
if (!cb_info[0].IsBuffer()) {
133179
throw Napi::TypeError::New(env,
134180
"First argument must be a Buffer containing serialized AvmFastSimulationInputs");
135181
}
136-
137-
if (!cb_info[1].IsObject()) {
138-
throw Napi::TypeError::New(env, "Second argument must be a contractProvider object");
139-
}
140-
141-
if (!cb_info[2].IsExternal()) {
142-
throw Napi::TypeError::New(env, "Third argument must be a WorldState handle (External)");
143-
}
144-
145-
if (!cb_info[3].IsNumber()) {
146-
throw Napi::TypeError::New(env, "Fourth argument must be a log level number (0-7)");
147-
}
148-
149-
// Extract optional cancellation token (5th argument)
150-
avm2::simulation::CancellationTokenPtr cancellation_token = nullptr;
151-
if (cb_info.Length() > 4 && cb_info[4].IsExternal()) {
152-
auto token_external = cb_info[4].As<Napi::External<avm2::simulation::CancellationToken>>();
153-
// Wrap the raw pointer in a shared_ptr that does NOT delete (since the External owns it)
154-
cancellation_token = std::shared_ptr<avm2::simulation::CancellationToken>(
155-
token_external.Data(), [](avm2::simulation::CancellationToken*) {
156-
// No-op deleter: the External
157-
// (via shared_ptr destructor
158-
// callback) owns the token
159-
});
160-
}
161-
162-
// Extract log level and set logging flags
163-
int log_level = cb_info[3].As<Napi::Number>().Int32Value();
164-
set_logging_from_level(log_level);
165-
166182
// Extract the inputs buffer
167183
auto inputs_buffer = cb_info[0].As<Napi::Buffer<uint8_t>>();
168184
size_t length = inputs_buffer.Length();
169-
170185
// Copy the buffer data into C++ memory (we can't access Napi objects from worker thread)
171186
auto data = std::make_shared<std::vector<uint8_t>>(inputs_buffer.Data(), inputs_buffer.Data() + length);
172187

188+
/***********************************
189+
*** ContractProvider (required) ***
190+
***********************************/
191+
if (!cb_info[1].IsObject()) {
192+
throw Napi::TypeError::New(env, "Second argument must be a contractProvider object");
193+
}
173194
// Extract and validate contract provider callbacks
174195
auto contract_provider = cb_info[1].As<Napi::Object>();
175196
ContractCallbacks::validate(env, contract_provider);
176-
177197
// Create thread-safe function wrappers for callbacks
178198
// These allow us to call TypeScript from the C++ worker thread
179199
ContractTsfns tsfns{
@@ -196,18 +216,68 @@ Napi::Value AvmSimulateNapi::simulate(const Napi::CallbackInfo& cb_info)
196216
env, ContractCallbacks::get(contract_provider, CALLBACK_REVERT_CHECKPOINT), CALLBACK_REVERT_CHECKPOINT),
197217
};
198218

219+
/*****************************
220+
*** WorldState (required) ***
221+
*****************************/
222+
if (!cb_info[2].IsExternal()) {
223+
throw Napi::TypeError::New(env, "Third argument must be a WorldState handle (External)");
224+
}
199225
// Extract WorldState handle (3rd argument)
200226
auto external = cb_info[2].As<Napi::External<world_state::WorldState>>();
201227
world_state::WorldState* ws_ptr = external.Data();
202228

203-
// Create a deferred promise
204-
auto deferred = std::make_shared<Napi::Promise::Deferred>(env);
229+
/***************************
230+
*** LogLevel (optional) ***
231+
***************************/
232+
int log_level = -1;
233+
if (cb_info.Length() > 3 && cb_info[3].IsNumber()) {
234+
log_level = cb_info[3].As<Napi::Number>().Int32Value();
235+
set_logging_from_level(log_level);
236+
}
237+
238+
/*********************************
239+
*** LoggerFunction (optional) ***
240+
*********************************/
241+
std::shared_ptr<Napi::ThreadSafeFunction> logger_tsfn = nullptr;
242+
if (cb_info.Length() > 4 && !cb_info[4].IsNull() && !cb_info[4].IsUndefined()) {
243+
if (cb_info[4].IsFunction()) {
244+
// Logger function provided - create thread-safe wrapper
245+
auto logger_function = cb_info[4].As<Napi::Function>();
246+
logger_tsfn = make_tsfn(env, logger_function, "LoggerCallback");
247+
// Create LogFunction wrapper and set it as the global log function
248+
// This will be used by C++ logging macros (info, debug, vinfo, important)
249+
set_log_function(create_log_function_from_tsfn(logger_tsfn));
250+
} else {
251+
throw Napi::TypeError::New(env, "Fifth argument must be a logger function, null, or undefined");
252+
}
253+
}
254+
255+
/*************************************
256+
*** Cancellation Token (optional) ***
257+
*************************************/
258+
avm2::simulation::CancellationTokenPtr cancellation_token = nullptr;
259+
if (cb_info.Length() > 5 && cb_info[5].IsExternal()) {
260+
auto token_external = cb_info[5].As<Napi::External<avm2::simulation::CancellationToken>>();
261+
// Wrap the raw pointer in a shared_ptr that does NOT delete (since the External owns it)
262+
cancellation_token = std::shared_ptr<avm2::simulation::CancellationToken>(
263+
token_external.Data(), [](avm2::simulation::CancellationToken*) {
264+
// No-op deleter: the External (via shared_ptr destructor callback) owns the token
265+
});
266+
}
267+
268+
/**********************************************************
269+
*** Create Deferred Promise and launch async operation ***
270+
**********************************************************/
205271

272+
auto deferred = std::make_shared<Napi::Promise::Deferred>(env);
206273
// Create async operation that will run on a worker thread
207-
auto* op =
208-
new AsyncOperation(env, deferred, [data, tsfns, ws_ptr, cancellation_token](msgpack::sbuffer& result_buffer) {
274+
auto* op = new AsyncOperation(
275+
env, deferred, [data, tsfns, logger_tsfn, ws_ptr, cancellation_token](msgpack::sbuffer& result_buffer) {
276+
// Collect all thread-safe functions including logger for cleanup
277+
auto all_tsfns = tsfns.to_vector();
278+
all_tsfns.push_back(logger_tsfn);
209279
// Ensure all thread-safe functions are released in all code paths
210-
TsfnReleaser releaser = TsfnReleaser(tsfns.to_vector());
280+
TsfnReleaser releaser = TsfnReleaser(std::move(all_tsfns));
211281

212282
try {
213283
// Deserialize inputs from msgpack

barretenberg/cpp/src/barretenberg/vm2/constraining/prover.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void AvmProver::execute_preamble_round()
4949
{
5050
FF vk_hash = vk->hash();
5151
transcript->add_to_hash_buffer("avm_vk_hash", vk_hash);
52-
info("AVM vk hash in prover: ", vk_hash);
52+
vinfo("AVM vk hash in prover: ", vk_hash);
5353
}
5454

5555
/**

barretenberg/cpp/src/barretenberg/vm2/proving_helper.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ AvmProvingHelper::VkData AvmProvingHelper::get_verification_key()
4444
auto verification_key =
4545
std::make_shared<AvmVerifier::VerificationKey>(constraining::AvmFixedVKCommitments::get_all());
4646

47-
info("AVM vk hash: ", verification_key->hash());
47+
vinfo("AVM vk hash: ", verification_key->hash());
4848

4949
auto serialized_vk = to_buffer(verification_key->to_field_elements());
5050

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/execution.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,7 @@ EnqueuedCallResult Execution::execute(std::unique_ptr<ContextInterface> enqueued
18091809
} catch (const std::exception& e) {
18101810
// This is a coding error, we should not get here.
18111811
// All exceptions should fall in the above catch blocks.
1812-
info("An unhandled exception occurred: ", e.what());
1812+
important("An unhandled exception occurred: ", e.what());
18131813
throw e;
18141814
}
18151815

barretenberg/cpp/src/barretenberg/vm2/simulation/gadgets/tx_execution.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ TxExecutionResult TxExecution::simulate(const Tx& tx)
255255
merkle_db.commit_checkpoint();
256256
contract_db.commit_checkpoint();
257257
} catch (const TxExecutionException& e) {
258-
info("Teardown failure while simulating tx ", tx.hash, ": ", e.what());
258+
important("Teardown failure while simulating tx ", tx.hash, ": ", e.what());
259259
tx_context.revert_code = tx_context.revert_code == RevertCode::APP_LOGIC_REVERTED
260260
? RevertCode::BOTH_REVERTED
261261
: RevertCode::TEARDOWN_REVERTED;

barretenberg/cpp/src/barretenberg/vm2/simulation/standalone/hybrid_execution.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ EnqueuedCallResult HybridExecution::execute(std::unique_ptr<ContextInterface> en
8787
} catch (const std::exception& e) {
8888
// This is a coding error, we should not get here.
8989
// All exceptions should fall in the above catch blocks.
90-
info("An unhandled exception occurred: ", e.what());
90+
important("An unhandled exception occurred: ", e.what());
9191
throw e;
9292
}
9393

barretenberg/cpp/src/barretenberg/vm2/simulation_helper.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ TxSimulationResult AvmSimulationHelper::simulate_fast_internal(ContractDBInterfa
478478
const DebugLogLevel debug_log_level = DebugLogLevel::INFO;
479479
return std::make_unique<DebugLogger>(debug_log_level,
480480
config.collection_limits.max_debug_log_memory_reads,
481-
[](const std::string& message) { info(message); });
481+
[](const std::string& message) { vinfo(message); });
482482
} else {
483483
return std::make_unique<NoopDebugLogger>();
484484
}

0 commit comments

Comments
 (0)