Skip to content

Commit 28ebc53

Browse files
committed
feat(devtools): IMC flow monitor, per-module memory attribution, log stream panels
2 parents a0e7a27 + d7f3593 commit 28ebc53

30 files changed

Lines changed: 2110 additions & 17 deletions

include/bml_imc_bus.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,69 @@ typedef BML_Result (*PFN_BML_ImcGetTopicName)(BML_Context ctx,
403403
size_t buffer_size,
404404
size_t *out_length);
405405

406+
/* ========================================================================
407+
* Message Flow Monitoring APIs
408+
* ======================================================================== */
409+
410+
/**
411+
* @brief Trace record for a published message.
412+
*
413+
* Captures metadata about a message at publish time for flow monitoring.
414+
*/
415+
typedef struct BML_ImcMessageTrace {
416+
size_t struct_size; /**< sizeof(BML_ImcMessageTrace), must be first */
417+
BML_TopicId topic; /**< Topic ID */
418+
BML_Mod owner; /**< Publishing module */
419+
uint64_t timestamp_qpc; /**< QueryPerformanceCounter timestamp */
420+
size_t payload_size; /**< Size of payload in bytes */
421+
uint32_t payload_type_id; /**< Payload type ID */
422+
uint32_t priority; /**< Message priority */
423+
uint32_t flags; /**< Message flags */
424+
} BML_ImcMessageTrace;
425+
426+
/**
427+
* @brief Callback invoked when a message is published.
428+
*
429+
* Fired from owner-variant publish methods (Publish/PublishEx/PublishBuffer/
430+
* PublishInterceptable/PublishState) after validation but before dispatch.
431+
* Not invoked for internal publishes or non-owner variants.
432+
*
433+
* @param[in] trace Trace record with message metadata
434+
* @param[in] user_data Opaque pointer provided at registration
435+
*
436+
* @threadsafe Yes (invoked from publish path)
437+
* @warning Must be fast and non-blocking. Do not call IMC Pump from tap.
438+
*/
439+
typedef void (*BML_ImcMessageTap)(const BML_ImcMessageTrace *trace, void *user_data);
440+
441+
/**
442+
* @brief Register a message tap for flow monitoring.
443+
*
444+
* Only one tap can be active at a time. Overwrites any previous tap.
445+
* Use for DevTools, debuggers, or telemetry systems.
446+
*
447+
* @param[in] owner Module handle (owner of tap)
448+
* @param[in] tap Tap callback (NULL to unregister)
449+
* @param[in] user_data Opaque pointer passed to tap
450+
* @return BML_RESULT_OK on success
451+
*
452+
* @threadsafe Yes
453+
*/
454+
typedef BML_Result (*PFN_BML_ImcRegisterMessageTap)(BML_Mod owner,
455+
BML_ImcMessageTap tap,
456+
void *user_data);
457+
458+
/**
459+
* @brief Unregister the current message tap.
460+
*
461+
* @param[in] owner Module handle (must match registration owner)
462+
* @return BML_RESULT_OK on success
463+
* @return BML_RESULT_NOT_FOUND if no tap registered or owner mismatch
464+
*
465+
* @threadsafe Yes
466+
*/
467+
typedef BML_Result (*PFN_BML_ImcUnregisterMessageTap)(BML_Mod owner);
468+
406469
/* ========================================================================
407470
* Bus Interface Vtable
408471
* ======================================================================== */
@@ -431,6 +494,8 @@ typedef struct BML_ImcBusInterface {
431494
PFN_BML_ImcResetStats ResetStats;
432495
PFN_BML_ImcGetTopicInfo GetTopicInfo;
433496
PFN_BML_ImcGetTopicName GetTopicName;
497+
PFN_BML_ImcRegisterMessageTap RegisterMessageTap;
498+
PFN_BML_ImcUnregisterMessageTap UnregisterMessageTap;
434499
} BML_ImcBusInterface;
435500

436501
BML_END_CDECLS

include/bml_logging.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ typedef void (*BML_LogSinkDispatchFn)(BML_Context ctx, const BML_LogMessageInfo
5555
*/
5656
typedef void (*BML_LogSinkShutdownFn)(void *user_data);
5757

58+
/**
59+
* @brief Log listener callback type (non-exclusive, multiple allowed)
60+
* @param ctx BML context
61+
* @param info Log message information
62+
* @param user_data User-provided context from registration
63+
* @note This callback is invoked for every log message, independent of sink override.
64+
* The callback should be fast and avoid blocking operations.
65+
*/
66+
typedef void (*BML_LogListenerFn)(BML_Context ctx, const BML_LogMessageInfo *info, void *user_data);
67+
5868
/**
5969
* @brief Flags for log sink override behavior
6070
*/
@@ -123,6 +133,30 @@ typedef void (*PFN_BML_LogVa)(BML_Mod owner,
123133
*/
124134
typedef void (*PFN_BML_SetLogFilter)(BML_Mod owner, BML_LogSeverity minimum_level);
125135

136+
/**
137+
* @brief Add a log listener callback
138+
* @param owner Module registering the listener
139+
* @param listener Callback function to receive log messages
140+
* @param user_data User context passed to the callback
141+
* @return BML_RESULT_OK on success
142+
* @return BML_RESULT_INVALID_ARGUMENT if listener is NULL
143+
* @threadsafe Yes
144+
*/
145+
typedef BML_Result (*PFN_BML_AddLogListener)(BML_Mod owner,
146+
BML_LogListenerFn listener,
147+
void *user_data);
148+
149+
/**
150+
* @brief Remove a log listener callback
151+
* @param owner Module that registered the listener
152+
* @param listener Callback function to remove
153+
* @return BML_RESULT_OK on success
154+
* @return BML_RESULT_NOT_FOUND if listener was not registered
155+
* @threadsafe Yes
156+
*/
157+
typedef BML_Result (*PFN_BML_RemoveLogListener)(BML_Mod owner,
158+
BML_LogListenerFn listener);
159+
126160
typedef enum BML_LogCreateFlags {
127161
BML_LOG_CREATE_ALLOW_TAGS = 1u << 0,
128162
BML_LOG_CREATE_ALLOW_FILTER = 1u << 1,
@@ -155,6 +189,8 @@ typedef struct BML_CoreLoggingInterface {
155189
PFN_BML_SetLogFilter SetLogFilter;
156190
PFN_BML_RegisterLogSinkOverride RegisterSinkOverride;
157191
PFN_BML_ClearLogSinkOverride ClearSinkOverride;
192+
PFN_BML_AddLogListener AddLogListener;
193+
PFN_BML_RemoveLogListener RemoveLogListener;
158194
} BML_CoreLoggingInterface;
159195

160196

include/bml_memory.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,46 @@ typedef struct BML_MemoryStats {
191191
*/
192192
typedef BML_Result (*PFN_BML_GetMemoryStats)(BML_Context ctx, BML_MemoryStats *out_stats);
193193

194+
/* ========== Per-module Memory Tracking ========== */
195+
196+
/**
197+
* @brief Per-module memory statistics (for diagnostic enumeration)
198+
*/
199+
typedef struct BML_ModuleMemoryStats {
200+
size_t struct_size; /**< sizeof(BML_ModuleMemoryStats) */
201+
const char *module_id; /**< Stable pointer, valid during callback */
202+
uint64_t total_allocated; /**< Bytes currently held by this module */
203+
uint64_t peak_allocated; /**< Peak bytes held */
204+
uint64_t alloc_count; /**< Lifetime allocation count */
205+
uint64_t free_count; /**< Lifetime free count */
206+
uint64_t active_alloc_count; /**< Currently live allocations */
207+
} BML_ModuleMemoryStats;
208+
209+
#define BML_MODULE_MEMORY_STATS_INIT { sizeof(BML_ModuleMemoryStats), NULL, 0, 0, 0, 0, 0 }
210+
211+
/**
212+
* @brief Callback for per-module memory enumeration
213+
*/
214+
typedef void (*BML_EnumerateModuleMemoryFn)(
215+
const BML_ModuleMemoryStats *stats, void *user_data);
216+
217+
/**
218+
* @brief Enable/disable per-module memory tracking.
219+
* When enabled, each alloc/free is attributed to the calling module.
220+
* Adds ~100-200ns overhead per allocation.
221+
* @threadsafe Yes
222+
*/
223+
typedef BML_Result (*PFN_BML_EnableModuleMemoryTracking)(
224+
BML_Context ctx, BML_Bool enable);
225+
226+
/**
227+
* @brief Enumerate per-module memory statistics.
228+
* Calls callback once per tracked module.
229+
* @threadsafe Yes (snapshot under lock)
230+
*/
231+
typedef BML_Result (*PFN_BML_EnumerateModuleMemory)(
232+
BML_Context ctx, BML_EnumerateModuleMemoryFn callback, void *user_data);
233+
194234
typedef struct BML_CoreMemoryInterface {
195235
BML_InterfaceHeader header;
196236
BML_Context Context;
@@ -205,6 +245,8 @@ typedef struct BML_CoreMemoryInterface {
205245
PFN_BML_MemoryPoolFree MemoryPoolFree;
206246
PFN_BML_MemoryPoolDestroy MemoryPoolDestroy;
207247
PFN_BML_GetMemoryStats GetMemoryStats;
248+
PFN_BML_EnableModuleMemoryTracking EnableModuleMemoryTracking;
249+
PFN_BML_EnumerateModuleMemory EnumerateModuleMemory;
208250
} BML_CoreMemoryInterface;
209251

210252

modules/BML_DevTools/locale/en.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ imc = "IMC"
99
interfaces = "Interfaces"
1010
hooks = "Hooks"
1111
modules = "Modules"
12+
imc_flow = "IMC Flow"
13+
module_memory = "Module Memory"
14+
log_stream = "Log Stream"
1215

1316
[column]
1417
id = "ID"
@@ -25,6 +28,31 @@ target = "Target"
2528
address = "Address"
2629
priority = "Priority"
2730
module_id = "Module ID"
31+
time = "Time"
32+
topic = "Topic"
33+
source = "Source"
34+
size = "Size"
35+
rate = "Rate/s"
36+
allocated = "Allocated"
37+
peak = "Peak"
38+
active = "Active"
39+
trend = "Trend"
40+
level = "Level"
41+
module = "Module"
42+
tag = "Tag"
43+
message = "Message"
2844

2945
[status]
3046
conflict = "CONFLICT"
47+
48+
[label]
49+
feature_unavailable = "Feature requires BML >= 0.4.1"
50+
throughput = "Throughput"
51+
pause = "Pause"
52+
clear = "Clear"
53+
filter = "Filter"
54+
tracking_active = "Per-module tracking active"
55+
tracking_note = "Tracking adds ~100-200ns overhead per allocation"
56+
total_across = "Total: %.2f MB across %u modules"
57+
severity_filter = "Severity"
58+
auto_scroll = "Auto-scroll"

modules/BML_DevTools/locale/zh-CN.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ imc = "IMC 消息总线"
99
interfaces = "接口注册表"
1010
hooks = "Hook 注册表"
1111
modules = "已加载模块"
12+
imc_flow = "IMC 消息流"
13+
module_memory = "模块内存"
14+
log_stream = "日志流"
1215

1316
[column]
1417
id = "ID"
@@ -25,6 +28,31 @@ target = "目标"
2528
address = "地址"
2629
priority = "优先级"
2730
module_id = "模块 ID"
31+
time = "时间"
32+
topic = "主题"
33+
source = "来源"
34+
size = "大小"
35+
rate = "速率/秒"
36+
allocated = "已分配"
37+
peak = "峰值"
38+
active = "活跃"
39+
trend = "趋势"
40+
level = "级别"
41+
module = "模块"
42+
tag = "标签"
43+
message = "消息"
2844

2945
[status]
3046
conflict = "冲突"
47+
48+
[label]
49+
feature_unavailable = "此功能需要 BML >= 0.4.1"
50+
throughput = "吞吐量"
51+
pause = "暂停"
52+
clear = "清除"
53+
filter = "过滤"
54+
tracking_active = "模块内存跟踪已启用"
55+
tracking_note = "跟踪每次分配增加约 100-200ns 开销"
56+
total_across = "合计: %.2f MB, 共 %u 个模块"
57+
severity_filter = "严重度"
58+
auto_scroll = "自动滚动"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#ifndef BML_DEVTOOLS_CONCURRENT_RING_BUFFER_H
2+
#define BML_DEVTOOLS_CONCURRENT_RING_BUFFER_H
3+
4+
#include <atomic>
5+
#include <cstddef>
6+
#include <new>
7+
#include <type_traits>
8+
9+
namespace devtools {
10+
11+
/// Lock-free bounded MPSC (multi-producer, single-consumer) ring buffer.
12+
/// Uses Vyukov sequence-based algorithm for safe concurrent writes.
13+
/// N must be a power of two.
14+
template <typename T, size_t N>
15+
class ConcurrentRingBuffer {
16+
static_assert((N & (N - 1)) == 0, "N must be power of 2");
17+
static_assert(N >= 2, "N must be at least 2");
18+
static constexpr size_t kMask = N - 1;
19+
20+
struct Slot {
21+
std::atomic<size_t> sequence;
22+
T data;
23+
};
24+
25+
alignas(64) Slot m_Slots[N];
26+
alignas(64) std::atomic<size_t> m_WriteHead{0};
27+
alignas(64) size_t m_ReadTail{0};
28+
29+
public:
30+
ConcurrentRingBuffer() {
31+
for (size_t i = 0; i < N; ++i)
32+
m_Slots[i].sequence.store(i, std::memory_order_relaxed);
33+
}
34+
35+
bool TryPush(const T &value) {
36+
size_t pos = m_WriteHead.load(std::memory_order_relaxed);
37+
for (;;) {
38+
Slot &slot = m_Slots[pos & kMask];
39+
size_t seq = slot.sequence.load(std::memory_order_acquire);
40+
auto diff = static_cast<intptr_t>(seq) - static_cast<intptr_t>(pos);
41+
if (diff == 0) {
42+
if (m_WriteHead.compare_exchange_weak(
43+
pos, pos + 1, std::memory_order_relaxed)) {
44+
slot.data = value;
45+
slot.sequence.store(pos + 1, std::memory_order_release);
46+
return true;
47+
}
48+
} else if (diff < 0) {
49+
return false;
50+
} else {
51+
pos = m_WriteHead.load(std::memory_order_relaxed);
52+
}
53+
}
54+
}
55+
56+
size_t Drain(T *out, size_t max_count) {
57+
size_t count = 0;
58+
while (count < max_count) {
59+
Slot &slot = m_Slots[m_ReadTail & kMask];
60+
size_t seq = slot.sequence.load(std::memory_order_acquire);
61+
auto diff = static_cast<intptr_t>(seq)
62+
- static_cast<intptr_t>(m_ReadTail + 1);
63+
if (diff == 0) {
64+
out[count++] = std::move(slot.data);
65+
slot.sequence.store(m_ReadTail + N, std::memory_order_release);
66+
++m_ReadTail;
67+
} else {
68+
break;
69+
}
70+
}
71+
return count;
72+
}
73+
};
74+
75+
} // namespace devtools
76+
77+
#endif // BML_DEVTOOLS_CONCURRENT_RING_BUFFER_H

modules/BML_DevTools/src/DevToolsMod.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
#include "InterfacePanel.h"
2727
#include "HookPanel.h"
2828
#include "ModulePanel.h"
29+
#include "ImcFlowPanel.h"
30+
#include "ModuleMemoryPanel.h"
31+
#include "LogStreamPanel.h"
2932

3033
namespace {
3134

@@ -47,6 +50,9 @@ class DevToolsMod : public bml::Module {
4750
m_Panels.push_back(std::make_unique<devtools::InterfacePanel>());
4851
m_Panels.push_back(std::make_unique<devtools::HookPanel>());
4952
m_Panels.push_back(std::make_unique<devtools::ModulePanel>());
53+
m_Panels.push_back(std::make_unique<devtools::ImcFlowPanel>());
54+
m_Panels.push_back(std::make_unique<devtools::ModuleMemoryPanel>());
55+
m_Panels.push_back(std::make_unique<devtools::LogStreamPanel>());
5056
}
5157

5258
void RefreshAll() {
@@ -102,7 +108,12 @@ class DevToolsMod : public bml::Module {
102108
if (!event || event->repeat) return;
103109
if (event->key_code == 0x58) {
104110
m_Visible = !m_Visible;
105-
if (m_Visible) RefreshAll();
111+
if (m_Visible) {
112+
RefreshAll();
113+
for (auto &p : m_Panels) p->OnShow(Services());
114+
} else {
115+
for (auto &p : m_Panels) p->OnHide(Services());
116+
}
106117
}
107118
});
108119

@@ -114,6 +125,9 @@ class DevToolsMod : public bml::Module {
114125
}
115126

116127
void OnDetach() override {
128+
if (m_Visible) {
129+
for (auto &p : m_Panels) p->OnHide(Services());
130+
}
117131
m_Panels.clear();
118132
m_Subs.Clear();
119133
m_DrawReg.Reset();

0 commit comments

Comments
 (0)