Skip to content

Commit 6e06528

Browse files
authored
trace_region on Android and Windows (#58)
* Add basic Android impl * Add async support * Remove sync calls since they don't really work * Add comments for existing Apple impl * Initial windows impl * Switch to logman * Regions of Interest
1 parent 53e5b05 commit 6e06528

3 files changed

Lines changed: 369 additions & 16 deletions

File tree

Source/Android/arcana/tracing/trace_region.h

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,40 @@
22
// Copyright (C) Microsoft Corporation. All rights reserved.
33
//
44

5+
//
6+
// Android trace_region implementation using the NDK ATrace API.
7+
//
8+
// This uses ATrace_beginAsyncSection/ATrace_endAsyncSection (API 29+) which pair
9+
// begin/end events by name + cookie, allowing trace regions to be moved across
10+
// threads (e.g. when an RAII trace_region is captured into an async continuation).
11+
// The sync API (ATrace_beginSection/ATrace_endSection) uses a per-thread stack,
12+
// which breaks when begin and end occur on different threads.
13+
//
14+
// When targeting minSdkVersion < 29, the async functions are resolved at runtime
15+
// via dlsym. If unavailable (device below API 29), ATrace is silently skipped
16+
// (logcat output at trace_level::log still works).
17+
//
18+
// NOTE: Async trace sections may not be visible in Android Studio's Profiler UI.
19+
// To view them, capture a trace with Perfetto:
20+
//
21+
// adb shell atrace --async_start -a <your.package.name> -c
22+
// # ... interact with the app ...
23+
// adb shell atrace --async_stop -o /data/local/tmp/trace.txt
24+
// adb pull /data/local/tmp/trace.txt
25+
//
26+
// Then open the trace file at https://ui.perfetto.dev
27+
//
28+
529
#pragma once
630

31+
#include <atomic>
32+
#include <string>
33+
#include <android/trace.h>
34+
#include <android/log.h>
35+
#if __ANDROID_MIN_SDK_VERSION__ < 29
36+
#include <dlfcn.h>
37+
#endif
38+
739
namespace arcana
840
{
941
enum class trace_level
@@ -12,33 +44,134 @@ namespace arcana
1244
log,
1345
};
1446

15-
// TODO: https://developer.android.com/topic/performance/tracing/custom-events-native
16-
// https://developer.android.com/ndk/reference/group/tracing
1747
class trace_region final
1848
{
1949
public:
2050
trace_region() = delete;
2151
trace_region(const trace_region&) = delete;
2252
trace_region& operator=(const trace_region&) = delete;
2353

24-
trace_region(const char*)
54+
trace_region(const char* name) :
55+
m_cookie{s_enabled ? s_nextCookie.fetch_add(1, std::memory_order_relaxed) : 0},
56+
m_name{m_cookie != 0 ? name : ""}
2557
{
58+
if (m_cookie != 0)
59+
{
60+
if (s_logEnabled)
61+
{
62+
__android_log_print(ANDROID_LOG_DEBUG, "trace_region", "[trace_region] BEGIN %s (cookie=%d, this=%p)", m_name.c_str(), m_cookie, static_cast<const void*>(this));
63+
}
64+
traceBegin(m_name.c_str(), m_cookie);
65+
}
2666
}
2767

28-
trace_region(trace_region&&) = default;
68+
// Move constructor transfers ownership; the moved-from region becomes inactive
69+
// (cookie set to 0) so its destructor won't emit a spurious end event.
70+
trace_region(trace_region&& other) :
71+
m_cookie{other.m_cookie},
72+
m_name{std::move(other.m_name)}
73+
{
74+
other.m_cookie = 0;
75+
}
2976

3077
~trace_region()
3178
{
79+
if (m_cookie != 0)
80+
{
81+
if (s_logEnabled)
82+
{
83+
__android_log_print(ANDROID_LOG_DEBUG, "trace_region", "[trace_region] END (cookie=%d, this=%p)", m_cookie, static_cast<const void*>(this));
84+
}
85+
traceEnd(m_name.c_str(), m_cookie);
86+
}
3287
}
3388

34-
trace_region& operator=(trace_region&&) = default;
89+
trace_region& operator=(trace_region&& other)
90+
{
91+
if (this == &other)
92+
{
93+
return *this;
94+
}
95+
96+
if (m_cookie != 0)
97+
{
98+
if (s_logEnabled)
99+
{
100+
__android_log_print(ANDROID_LOG_DEBUG, "trace_region", "[trace_region] END (move) (cookie=%d, this=%p)", m_cookie, static_cast<const void*>(this));
101+
}
102+
traceEnd(m_name.c_str(), m_cookie);
103+
}
104+
105+
m_cookie = other.m_cookie;
106+
m_name = std::move(other.m_name);
107+
other.m_cookie = 0;
35108

36-
static void enable(trace_level = trace_level::mark)
109+
return *this;
110+
}
111+
112+
static void enable(trace_level level = trace_level::mark)
37113
{
114+
if (s_enabled)
115+
{
116+
return;
117+
}
118+
119+
s_enabled = true;
120+
s_logEnabled = level == trace_level::log;
38121
}
39122

40123
static void disable()
41124
{
125+
if (!s_enabled)
126+
{
127+
return;
128+
}
129+
130+
s_enabled = false;
131+
s_logEnabled = false;
132+
}
133+
134+
private:
135+
static void traceBegin(const char* name, int32_t cookie)
136+
{
137+
#if __ANDROID_MIN_SDK_VERSION__ >= 29
138+
ATrace_beginAsyncSection(name, cookie);
139+
#else
140+
if (s_beginAsync)
141+
s_beginAsync(name, cookie);
142+
#endif
42143
}
144+
145+
static void traceEnd(const char* name, int32_t cookie)
146+
{
147+
#if __ANDROID_MIN_SDK_VERSION__ >= 29
148+
ATrace_endAsyncSection(name, cookie);
149+
#else
150+
if (s_endAsync)
151+
s_endAsync(name, cookie);
152+
#endif
153+
}
154+
155+
static inline std::atomic<bool> s_enabled{false};
156+
static inline std::atomic<bool> s_logEnabled{false};
157+
static inline std::atomic<int32_t> s_nextCookie{1};
158+
159+
#if __ANDROID_MIN_SDK_VERSION__ < 29
160+
// Resolve async trace functions at load time. These are available on
161+
// devices running API 29+, even when the app targets a lower minSdkVersion.
162+
using AsyncTraceFunc = void (*)(const char*, int32_t);
163+
static inline const AsyncTraceFunc s_beginAsync{
164+
reinterpret_cast<AsyncTraceFunc>(dlsym(RTLD_DEFAULT, "ATrace_beginAsyncSection"))};
165+
static inline const AsyncTraceFunc s_endAsync{
166+
reinterpret_cast<AsyncTraceFunc>(dlsym(RTLD_DEFAULT, "ATrace_endAsyncSection"))};
167+
#endif
168+
169+
// Cookie uniquely identifies this trace interval for the async API, analogous
170+
// to os_signpost_id_t on Apple. A cookie of 0 means the region is inactive.
171+
int32_t m_cookie;
172+
// Name is stored as std::string (not const char*) because callers may pass
173+
// c_str() from a temporary std::string, and ATrace_endAsyncSection needs the
174+
// name to match the corresponding begin call.
175+
std::string m_name;
43176
};
44177
}

Source/Apple/arcana/tracing/trace_region.h

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,30 @@
22
// Copyright (C) Microsoft Corporation. All rights reserved.
33
//
44

5+
//
6+
// Apple trace_region implementation using os_signpost.
7+
//
8+
// Signpost intervals are logged to the OS_LOG_CATEGORY_POINTS_OF_INTEREST category,
9+
// which makes them appear on the "Points of Interest" timeline in Instruments.
10+
// Each interval is identified by a unique os_signpost_id_t, allowing begin/end
11+
// pairs to be matched even across threads or when multiple regions overlap.
12+
//
13+
// To capture and view traces in Instruments:
14+
// 1. Open Instruments (Xcode → Open Developer Tool → Instruments, or ⌘I from Xcode)
15+
// 2. Choose the "Blank" template, then add the "os_signpost" instrument
16+
// (click "+", search for "os_signpost", and add it)
17+
// 3. Select your app as the target and click Record
18+
// 4. Interact with the app, then stop recording
19+
// 5. Trace regions appear on the "Points of Interest" timeline as labeled intervals
20+
// 6. Click on an interval to see its name and duration in the detail pane
21+
//
22+
523
#pragma once
624

725
#include <atomic>
826
#include <os/signpost.h>
927
#include <os/log.h>
1028

11-
#define SIGNPOST_NAME "trace_region"
12-
1329
namespace arcana
1430
{
1531
enum class trace_level
@@ -34,7 +50,7 @@ namespace arcana
3450
{
3551
os_log_debug(s_log, "[trace_region] BEGIN %s (id=%llu, this=%p)", name, m_id, this);
3652
}
37-
os_signpost_interval_begin(s_log, m_id, SIGNPOST_NAME, "%s", name);
53+
os_signpost_interval_begin(s_log, m_id, "trace_region", "%s", name);
3854
}
3955
}
4056

@@ -52,19 +68,24 @@ namespace arcana
5268
{
5369
os_log_debug(s_log, "[trace_region] END (id=%llu, this=%p)", m_id, this);
5470
}
55-
os_signpost_interval_end(s_log, m_id, SIGNPOST_NAME);
71+
os_signpost_interval_end(s_log, m_id, "trace_region");
5672
}
5773
}
5874

5975
trace_region& operator=(trace_region&& other)
6076
{
77+
if (this == &other)
78+
{
79+
return *this;
80+
}
81+
6182
if (m_id != OS_SIGNPOST_ID_NULL)
6283
{
6384
if (s_logEnabled)
6485
{
6586
os_log_debug(s_log, "[trace_region] END (move) (id=%llu, this=%p)", m_id, this);
6687
}
67-
os_signpost_interval_end(s_log, m_id, SIGNPOST_NAME);
88+
os_signpost_interval_end(s_log, m_id, "trace_region");
6889
}
6990

7091
m_id = other.m_id;
@@ -75,12 +96,22 @@ namespace arcana
7596

7697
static void enable(trace_level level = trace_level::mark)
7798
{
99+
if (s_enabled)
100+
{
101+
return;
102+
}
103+
78104
s_enabled = true;
79105
s_logEnabled = level == trace_level::log;
80106
}
81107

82108
static void disable()
83109
{
110+
if (!s_enabled)
111+
{
112+
return;
113+
}
114+
84115
s_enabled = false;
85116
s_logEnabled = false;
86117
}

0 commit comments

Comments
 (0)