Skip to content

Commit f18ce07

Browse files
NSHkrNSHkr
authored andcommitted
refactor instrumentation_runtime
1 parent 997dd20 commit f18ce07

8 files changed

Lines changed: 1487 additions & 1335 deletions

File tree

lib/elixir_scope/capture/instrumentation_runtime.ex

Lines changed: 92 additions & 1335 deletions
Large diffs are not rendered by default.

lib/elixir_scope/capture/instrumentation_runtime/ast_reporting.ex

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
defmodule ElixirScope.Capture.InstrumentationRuntime.Context do
2+
@moduledoc """
3+
Manages instrumentation context for processes.
4+
5+
Handles initialization, cleanup, and state management for instrumentation
6+
contexts including correlation IDs, call stacks, and enablement state.
7+
"""
8+
9+
alias ElixirScope.Capture.RingBuffer
10+
11+
@type correlation_id :: term()
12+
@type t :: %{
13+
buffer: RingBuffer.t() | nil,
14+
correlation_id: correlation_id(),
15+
call_stack: [correlation_id()],
16+
enabled: boolean()
17+
}
18+
19+
# Process dictionary keys for fast access
20+
@context_key :elixir_scope_context
21+
@call_stack_key :elixir_scope_call_stack
22+
23+
@doc """
24+
Initializes the instrumentation context for the current process.
25+
26+
This should be called when a process starts or when ElixirScope is enabled.
27+
"""
28+
@spec initialize_context() :: :ok
29+
def initialize_context do
30+
case get_buffer() do
31+
{:ok, buffer} ->
32+
context = %{
33+
buffer: buffer,
34+
correlation_id: nil,
35+
call_stack: [],
36+
enabled: true # For now, always enabled when buffer is available
37+
}
38+
39+
Process.put(@context_key, context)
40+
Process.put(@call_stack_key, [])
41+
:ok
42+
43+
{:error, _} ->
44+
# ElixirScope not available, set disabled context
45+
context = %{
46+
buffer: nil,
47+
correlation_id: nil,
48+
call_stack: [],
49+
enabled: false
50+
}
51+
52+
Process.put(@context_key, context)
53+
:ok
54+
end
55+
end
56+
57+
@doc """
58+
Clears the instrumentation context for the current process.
59+
"""
60+
@spec clear_context() :: :ok
61+
def clear_context do
62+
Process.delete(@context_key)
63+
Process.delete(@call_stack_key)
64+
:ok
65+
end
66+
67+
@doc """
68+
Checks if instrumentation is enabled for the current process.
69+
70+
This is the fastest possible check - just a process dictionary lookup.
71+
"""
72+
@spec enabled?() :: boolean()
73+
def enabled? do
74+
case Process.get(@context_key) do
75+
%{enabled: enabled} -> enabled
76+
_ -> false
77+
end
78+
end
79+
80+
@doc """
81+
Gets the current correlation ID (for nested calls).
82+
"""
83+
@spec current_correlation_id() :: correlation_id() | nil
84+
def current_correlation_id do
85+
case Process.get(@call_stack_key) do
86+
[current | _] -> current
87+
_ -> nil
88+
end
89+
end
90+
91+
@doc """
92+
Gets the current instrumentation context.
93+
"""
94+
@spec get_context() :: t()
95+
def get_context do
96+
Process.get(@context_key, %{enabled: false, buffer: nil, correlation_id: nil, call_stack: []})
97+
end
98+
99+
@doc """
100+
Temporarily disables instrumentation for the current process.
101+
102+
Useful for avoiding recursive instrumentation in ElixirScope's own code.
103+
"""
104+
@spec with_instrumentation_disabled((() -> term())) :: term()
105+
def with_instrumentation_disabled(fun) do
106+
old_context = Process.get(@context_key)
107+
108+
# Temporarily disable
109+
case old_context do
110+
%{} = context ->
111+
Process.put(@context_key, %{context | enabled: false})
112+
113+
_ ->
114+
Process.put(@context_key, %{enabled: false, buffer: nil, correlation_id: nil, call_stack: []})
115+
end
116+
117+
try do
118+
fun.()
119+
after
120+
# Restore old context
121+
if old_context do
122+
Process.put(@context_key, old_context)
123+
else
124+
Process.delete(@context_key)
125+
end
126+
end
127+
end
128+
129+
@doc """
130+
Generates a unique correlation ID.
131+
"""
132+
@spec generate_correlation_id() :: correlation_id()
133+
def generate_correlation_id do
134+
# Use a simple but unique correlation ID
135+
{System.monotonic_time(:nanosecond), self(), make_ref()}
136+
end
137+
138+
@doc """
139+
Pushes a correlation ID onto the call stack.
140+
"""
141+
@spec push_call_stack(correlation_id()) :: :ok
142+
def push_call_stack(correlation_id) do
143+
current_stack = Process.get(@call_stack_key, [])
144+
Process.put(@call_stack_key, [correlation_id | current_stack])
145+
:ok
146+
end
147+
148+
@doc """
149+
Pops a correlation ID from the call stack.
150+
"""
151+
@spec pop_call_stack() :: :ok
152+
def pop_call_stack do
153+
case Process.get(@call_stack_key, []) do
154+
[_ | rest] -> Process.put(@call_stack_key, rest)
155+
[] -> :ok
156+
end
157+
:ok
158+
end
159+
160+
# Private functions
161+
162+
defp get_buffer do
163+
# Try to get the main buffer from the application
164+
case Application.get_env(:elixir_scope, :main_buffer) do
165+
nil ->
166+
{:error, :no_buffer_configured}
167+
168+
buffer_name when is_atom(buffer_name) ->
169+
try do
170+
buffer_key = :"elixir_scope_buffer_#{buffer_name}"
171+
case :persistent_term.get(buffer_key, nil) do
172+
nil -> {:error, :buffer_not_found}
173+
buffer -> {:ok, buffer}
174+
end
175+
rescue
176+
_ -> {:error, :buffer_access_failed}
177+
end
178+
179+
buffer when is_map(buffer) ->
180+
{:ok, buffer}
181+
end
182+
end
183+
end
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
defmodule ElixirScope.Capture.InstrumentationRuntime.CoreReporting do
2+
@moduledoc """
3+
Core event reporting functionality for the instrumentation runtime.
4+
5+
Handles basic events like function calls, process spawns, message sends,
6+
state changes, and errors.
7+
"""
8+
9+
alias ElixirScope.Capture.{RingBuffer, Ingestor}
10+
alias ElixirScope.Capture.InstrumentationRuntime.Context
11+
12+
@type correlation_id :: Context.correlation_id()
13+
14+
@doc """
15+
Reports a function call entry.
16+
17+
This is called at the beginning of every instrumented function.
18+
Must be extremely fast - target <100ns when disabled, <500ns when enabled.
19+
"""
20+
@spec report_function_entry(module(), atom(), list()) :: correlation_id() | nil
21+
def report_function_entry(module, function, args) do
22+
case Context.get_context() do
23+
%{enabled: false} ->
24+
nil
25+
26+
%{enabled: true, buffer: buffer} when not is_nil(buffer) ->
27+
correlation_id = Context.generate_correlation_id()
28+
29+
# Push to call stack for nested tracking
30+
Context.push_call_stack(correlation_id)
31+
32+
# Ingest the event
33+
Ingestor.ingest_function_call(
34+
buffer,
35+
module,
36+
function,
37+
args,
38+
self(),
39+
correlation_id
40+
)
41+
42+
correlation_id
43+
44+
_ ->
45+
# ElixirScope not properly initialized
46+
nil
47+
end
48+
end
49+
50+
@doc """
51+
Reports function entry (4-arity version for compatibility).
52+
"""
53+
@spec report_function_entry(atom(), integer(), boolean(), term()) :: correlation_id() | nil
54+
def report_function_entry(function_name, _arity, capture_args, correlation_id) do
55+
case Context.get_context() do
56+
%{enabled: false} ->
57+
nil
58+
59+
%{enabled: true, buffer: buffer} when not is_nil(buffer) ->
60+
# Push to call stack for nested tracking
61+
Context.push_call_stack(correlation_id)
62+
63+
# Ingest the event
64+
Ingestor.ingest_function_call(
65+
buffer,
66+
__MODULE__, # Use a placeholder module since we don't have it here
67+
function_name,
68+
if(capture_args, do: [], else: :no_capture),
69+
self(),
70+
correlation_id
71+
)
72+
73+
correlation_id
74+
75+
_ ->
76+
# ElixirScope not properly initialized
77+
nil
78+
end
79+
end
80+
81+
@doc """
82+
Reports a function call exit.
83+
84+
This is called at the end of every instrumented function.
85+
"""
86+
@spec report_function_exit(correlation_id(), term(), non_neg_integer()) :: :ok
87+
def report_function_exit(correlation_id, return_value, duration_ns) do
88+
case Context.get_context() do
89+
%{enabled: false} ->
90+
:ok
91+
92+
%{enabled: true, buffer: buffer} when not is_nil(buffer) and not is_nil(correlation_id) ->
93+
# Pop from call stack
94+
Context.pop_call_stack()
95+
96+
# Ingest the return event
97+
Ingestor.ingest_function_return(
98+
buffer,
99+
return_value,
100+
duration_ns,
101+
correlation_id
102+
)
103+
104+
_ ->
105+
:ok
106+
end
107+
end
108+
109+
@doc """
110+
Reports function exit (5-arity version for compatibility).
111+
"""
112+
@spec report_function_exit(atom(), integer(), atom(), term(), term()) :: :ok
113+
def report_function_exit(_function_name, _arity, _exit_type, return_value, correlation_id) do
114+
case Context.get_context() do
115+
%{enabled: false} ->
116+
:ok
117+
118+
%{enabled: true, buffer: buffer} when not is_nil(buffer) and not is_nil(correlation_id) ->
119+
# Pop from call stack
120+
Context.pop_call_stack()
121+
122+
# Ingest the return event
123+
Ingestor.ingest_function_return(
124+
buffer,
125+
return_value,
126+
0, # Duration not available in this context
127+
correlation_id
128+
)
129+
130+
_ ->
131+
:ok
132+
end
133+
end
134+
135+
@doc """
136+
Reports a process spawn event.
137+
"""
138+
@spec report_process_spawn(pid()) :: :ok
139+
def report_process_spawn(child_pid) do
140+
case Context.get_context() do
141+
%{enabled: true, buffer: buffer} when not is_nil(buffer) ->
142+
Ingestor.ingest_process_spawn(buffer, self(), child_pid)
143+
144+
_ ->
145+
:ok
146+
end
147+
end
148+
149+
@doc """
150+
Reports a message send event.
151+
"""
152+
@spec report_message_send(pid(), term()) :: :ok
153+
def report_message_send(to_pid, message) do
154+
case Context.get_context() do
155+
%{enabled: true, buffer: buffer} when not is_nil(buffer) ->
156+
Ingestor.ingest_message_send(buffer, self(), to_pid, message)
157+
158+
_ ->
159+
:ok
160+
end
161+
end
162+
163+
@doc """
164+
Reports a state change event (for GenServer, Agent, etc.).
165+
"""
166+
@spec report_state_change(term(), term()) :: :ok
167+
def report_state_change(old_state, new_state) do
168+
case Context.get_context() do
169+
%{enabled: true, buffer: buffer} when not is_nil(buffer) ->
170+
Ingestor.ingest_state_change(buffer, self(), old_state, new_state)
171+
172+
_ ->
173+
:ok
174+
end
175+
end
176+
177+
@doc """
178+
Reports an error event.
179+
"""
180+
@spec report_error(term(), term(), list()) :: :ok
181+
def report_error(error, reason, stacktrace) do
182+
case Context.get_context() do
183+
%{enabled: true, buffer: buffer} when not is_nil(buffer) ->
184+
Ingestor.ingest_error(buffer, error, reason, stacktrace)
185+
186+
_ ->
187+
:ok
188+
end
189+
end
190+
end

0 commit comments

Comments
 (0)