Convert Streaming Call-Stack Samples into Function Events
AnthropicSoftware Engineer · Technical ScreenLast updated
Tests the ability to process discrete call-stack snapshots into properly nested, ordered function start and end events using longest common prefix analysis and stack unwinding.
Problem Description
Software profilers periodically take snapshots of an application's call stack. Given a sequence of timestamped call-stack samples and a final termination time end_time, convert these snapshots into an ordered list of function lifecycle events ("START" and "END").
Each sample is represented as a tuple (timestamp, stack), where stack is a list of strings representing the active call stack from the root caller (index 0) to the innermost active function (last index). Note that recursive calls may appear, meaning the same function name can appear multiple times at different depths.
To reconstruct the execution sequence:
- The system starts with an empty stack before the first sample.
- When transitioning between the previous stack and the current stack at
timestamp, identify the longest common prefix (LCP) of both stacks (matching both function name and depth index). - Any frames in the previous stack above the common prefix have exited. They must emit
"END"events in inside-out order (from the top of the previous stack down to the LCP boundary) at the currenttimestamp. - Any frames in the current stack above the common prefix have started. They must emit
"START"events in outside-in order (from the LCP boundary up to the top of the current stack) at the currenttimestamp. - At
end_time, any frames remaining on the stack must be closed by emitting"END"events in inside-out order.
Return the list of events. Each event is a tuple: (timestamp, event_type, function_name, depth), where depth is the 0-indexed position of the function frame in the stack.
Function Signature
def generate_call_events(
samples: list[tuple[int, list[str]]],
end_time: int
) -> list[tuple[int, str, str, int]]:
pass
Constraints
0 samples[-1][0]ifsamplesis non-empty.- Function names consist of non-empty alphanumeric strings.
Examples
Example 1
Input:
samples = [
(10, ["main", "foo"]),
(20, ["main", "bar"])
]
end_time = 30
Output:
[
(10, "START", "main", 0),
(10, "START", "foo", 1),
(20, "END", "foo", 1),
(20, "START", "bar", 1),
(30, "END", "bar", 1),
(30, "END", "main", 0)
]
Explanation: At t=10, both main and foo start. At t=20, foo has exited and bar has begun. At t=30 (end_time), bar and main exit.
Example 2 (Recursion)
Input:
samples = [
(100, ["eval", "eval", "eval"]),
(150, ["eval"])
]
end_time = 200
Output:
[
(100, "START", "eval", 0),
(100, "START", "eval", 1),
(100, "START", "eval", 2),
(150, "END", "eval", 2),
(150, "END", "eval", 1),
(200, "END", "eval", 0)
]
Hints
Hint 1
Find the length of the longest common prefix (LCP) between the previous call stack and the new call stack by comparing elements at matching indices.
Hint 2
Unwind the stack down to the LCP boundary by popping from the top of the stack first (inside-out) so that child frames end before their callers.
Hint 3
Push new frames above the LCP boundary from the lowest depth to highest depth (outside-in), and treat `end_time` simply as a transition to an empty stack `[]`.
Follow-up questions
- How would you refactor this implementation into a Python generator to stream events lazily with O(max_depth) memory overhead instead of materializing all events in memory?
- Because profilers sample at discrete intervals, short-lived function calls that start and finish between two ticks are missed entirely. How could you detect or statistically estimate these missing frames?
- If samples could arrive out of order or from multiple concurrent worker threads, what concurrency or buffering strategy would you use to output a coherent event trace?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.