Skip to content

Commit 75881c0

Browse files
Mobile Ads Developer Relationscopybara-github
authored andcommitted
Add support to emit CUIs with the duration specified.
PiperOrigin-RevId: 852491650
1 parent fbad52d commit 75881c0

2 files changed

Lines changed: 166 additions & 6 deletions

File tree

source/plugin/Assets/GoogleMobileAds/Common/Insight.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ namespace GoogleMobileAds.Common
77
[Serializable]
88
public class Insight
99
{
10+
/// <summary>
11+
/// Used to calculate timestamps since the Unix Epoch for .NET Framework 3.5+.
12+
/// </summary>
13+
internal static readonly DateTime UnixEpoch =
14+
new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
15+
1016
private static AdPlatform _platform;
1117
private static string _appId;
1218
private static string _appVersionName;
@@ -42,7 +48,7 @@ public Insight()
4248
{
4349
Success = true;
4450
StartTimeEpochMillis = (long)DateTime.UtcNow
45-
.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
51+
.Subtract(UnixEpoch)
4652
.TotalMilliseconds;
4753
Platform = _platform;
4854
AppId = _appId;
Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,175 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
24

35
namespace GoogleMobileAds.Common
46
{
7+
/// <summary>
8+
/// Serializable class to hold trace information.
9+
/// </summary>
10+
[Serializable]
11+
internal class TraceInfo
12+
{
13+
public string Id;
14+
/// <summary>
15+
/// The name of the trace operation. This is stored on the stack to be retrieved during
16+
/// `Dispose()` so the final end-of-trace insight can be emitted with the correct operation
17+
/// name.
18+
/// </summary>
19+
public string OperationName;
20+
public long StartTimeEpochMillis;
21+
}
22+
23+
/// <summary>
24+
/// Implements tracing by emitting insights (CUIs). It is safe to reuse the same `Tracer`
25+
/// instance across multiple threads because the instance itself is stateless, holding only a
26+
/// reference to the insights emitter. The actual trace context (the stack of active traces) is
27+
/// managed via the `ThreadStatic _traceStack' field.
28+
///
29+
/// This design ensures that each thread maintains its own independent trace tree structure
30+
/// (nested or single-level), preventing conflicts between concurrent operations on different
31+
/// threads.
32+
/// </summary>
533
public class Tracer : ITracer
634
{
7-
private class NoOpTrace : ITrace
35+
// `AsyncLocal` was intentionally skipped because we need to support .NET Framework 3.5+.
36+
[ThreadStatic]
37+
private static Stack<TraceInfo> _traceStack;
38+
private readonly IInsightsEmitter _insightsEmitter;
39+
40+
public Tracer(IInsightsEmitter insightsEmitter)
841
{
9-
public void Dispose() {}
42+
_insightsEmitter = insightsEmitter;
1043
}
1144

12-
private static readonly NoOpTrace _noOpTrace = new NoOpTrace();
45+
internal IInsightsEmitter Emitter
46+
{
47+
get { return _insightsEmitter; }
48+
}
1349

14-
public Tracer(IInsightsEmitter insightsEmitter) {}
50+
/// <summary>
51+
/// Retrieves the trace stack for the current thread.
52+
/// </summary>
53+
internal Stack<TraceInfo> GetTraceStack()
54+
{
55+
// We lazy-initialize the stack as a singleton to ensure each thread has its
56+
// own dedicated context for managing traces.
57+
if (_traceStack == null)
58+
{
59+
_traceStack = new Stack<TraceInfo>();
60+
}
61+
return _traceStack;
62+
}
1563

64+
/// <summary>
65+
/// Starts a new trace.
66+
/// </summary>
67+
/// <param name="name">The name of the trace operation.</param>
68+
/// <returns>A Trace instance.</returns>
1669
public ITrace StartTrace(string name)
1770
{
18-
return _noOpTrace;
71+
Stack<TraceInfo> traceStack = GetTraceStack();
72+
73+
// If there is already an active trace on this thread's stack,
74+
// it becomes the parent of the new trace we are starting.
75+
string parentId = traceStack.Count > 0 ? traceStack.Peek().Id : null;
76+
77+
long startTime = (long)DateTime.UtcNow
78+
.Subtract(Insight.UnixEpoch)
79+
.TotalMilliseconds;
80+
81+
TraceInfo traceInfo = new TraceInfo
82+
{
83+
Id = Guid.NewGuid().ToString(),
84+
OperationName = name,
85+
StartTimeEpochMillis = startTime,
86+
};
87+
88+
traceStack.Push(traceInfo);
89+
90+
_insightsEmitter.Emit(new Insight
91+
{
92+
Tracing = new Insight.TracingActivity
93+
{
94+
Id = traceInfo.Id,
95+
OperationName = name,
96+
ParentId = parentId,
97+
}
98+
});
99+
100+
return new Trace(traceInfo, parentId, this);
101+
}
102+
103+
/// <summary>
104+
/// Represents an active trace.
105+
/// </summary>
106+
private class Trace : ITrace
107+
{
108+
private readonly TraceInfo _traceInfo;
109+
private readonly string _parentId;
110+
private readonly Tracer _tracer;
111+
private bool _isDisposed = false;
112+
private readonly object _disposeLock = new object();
113+
114+
public Trace(TraceInfo traceInfo, string parentId, Tracer tracer)
115+
{
116+
_traceInfo = traceInfo;
117+
// We capture the parentId on init to ensure the end-of-trace insight is consistent
118+
// with the start-of-trace insight, even if the stack state is modified in between.
119+
_parentId = parentId;
120+
_tracer = tracer;
121+
}
122+
123+
/// <summary>
124+
/// Ends the trace, emits an end-of-trace insight, and cleans up the trace stack.
125+
/// This method is thread-safe and idempotent, ensuring that it can be called
126+
/// multiple times without causing errors or duplicate insights.
127+
/// </summary>
128+
public void Dispose()
129+
{
130+
// Ensures thread-safe idempotency for the disposal logic.
131+
lock (_disposeLock)
132+
{
133+
if (_isDisposed) return;
134+
_isDisposed = true;
135+
}
136+
// If LIFO order is maintained, verify current trace is top
137+
// before popping to ensure the trace tree structure remains valid.
138+
Stack<TraceInfo> traceStack = _tracer.GetTraceStack();
139+
if (traceStack.Count > 0 && traceStack.Peek().Id == _traceInfo.Id)
140+
{
141+
traceStack.Pop();
142+
}
143+
144+
// If LIFO order is broken, this branch will handle cleanup.
145+
else if (traceStack.Count > 0)
146+
{
147+
// If top of the stack is not the current trace, this indicates that traces are
148+
// not being disposed in LIFO order (e.g., parent disposed before child), or
149+
// traces are being disposed on different threads without context.
150+
// The stack is likely corrupted, so we clear it to prevent further issues.
151+
traceStack.Clear();
152+
}
153+
154+
long endTime = (long)DateTime.UtcNow
155+
.Subtract(Insight.UnixEpoch)
156+
.TotalMilliseconds;
157+
158+
// Ensure duration is non-negative.
159+
long duration = Math.Max(0, endTime - _traceInfo.StartTimeEpochMillis);
160+
161+
_tracer.Emitter.Emit(new Insight
162+
{
163+
Tracing = new Insight.TracingActivity
164+
{
165+
Id = _traceInfo.Id,
166+
OperationName = _traceInfo.OperationName,
167+
ParentId = _parentId,
168+
DurationMillis = duration,
169+
HasEnded = true,
170+
}
171+
});
172+
}
19173
}
20174
}
21175
}

0 commit comments

Comments
 (0)