Generate Function Profiling Events from Stack Samples
Company: Anthropic
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
# Generate Function Profiling Events from Stack Samples
You receive timestamped call-stack samples in increasing timestamp order. Each sample is `(timestamp, stack)`, where `stack` lists function names from the outermost call to the currently executing function.
Implement `profiling_events(samples, n)` and return events `(timestamp, type, function)` where `type` is `START` or `END`.
For `n = 1`, compare each accepted stack with the previous accepted stack: end functions removed from the suffix, deepest first, then start functions added to the suffix, outermost first. A function still in the common prefix emits no event.
For `n > 1`, debounce each frame independently rather than waiting for an identical complete stack. Identify a frame by its depth and function name under normal stack-prefix semantics. A candidate frame becomes active after it appears at the same depth, with the same ancestor prefix, in at least `n` consecutive samples; emit its `START` at the first timestamp in that run. The same run may confirm multiple previously inactive frames in a stable prefix at once: if a new stack `[A, B]` persists for `n` samples, both `A` and `B` start at the first sample's timestamp. An active frame ends after it is absent or replaced for at least `n` consecutive samples; emit its `END` at the first timestamp of that absence, deepest first. A shorter appearance or absence is transient and emits nothing. A deeper frame cannot remain active after an ancestor ends. At end of input, emit `END` events for every still-active frame at the last sample timestamp, deepest first.
## Constraints
- `1 <= n <= len(samples) <= 100000`
- Timestamps are strictly increasing integers.
- A stack contains unique non-empty function names and has depth at most `1000`.
- The first accepted stack transitions from an empty stack.
## Example
With `n = 1`, samples `(10, [A, B])`, `(20, [A, C])`, `(30, [])` produce:
```text
(10, START, A), (10, START, B),
(20, END, B), (20, START, C),
(30, END, C), (30, END, A)
```
With `n = 2`, samples `(10, [A, B])`, `(20, [A, C])`, `(30, [A, C])`, `(40, [])`, `(50, [])` start `A` at timestamp `10` even though its deeper frame changes, start `C` at timestamp `20`, and end `C` then `A` at timestamp `40`. `B` never becomes active.
## Clarifications
For a confirmed per-frame run, retain its first timestamp even though confirmation arrives later. When several events share a timestamp, emit ends deepest first and then starts outermost first.
## Hints
Track consecutive presence or absence per stack depth. Common prefixes identify surviving ancestors, but changing a deeper suffix must not reset an unchanged outer frame's confirmation count.
## Extensions
- Process samples online while delaying output until a run is confirmed.
- Support a time-duration threshold instead of a sample-count threshold.
- Bound memory when stack depth is large.
Quick Answer: Generate function start and end events from timestamped stack samples, including independently debounced frames across consecutive samples. Address transient appearances and absences, changing stack suffixes, shared timestamps, deep stacks, end-of-input behavior, complexity, and online processing.
A sampling profiler wakes the target thread at fixed instants and records its call stack. You are given those samples in strictly increasing timestamp order, and you must turn the *changes* between them into a stream of `START` / `END` events, one pair per function activation.
Each sample is a pair `(timestamp, stack)`. `stack` lists function names from the outermost call to the currently executing function, so `stack[0]` is the outermost frame and the last element is the innermost one. An empty stack means nothing was running at that instant. The stack in force before the first sample is empty.
Implement `profiling_events(samples, n)`. Return the list of events, each event a triple `[timestamp, type, function]` where `type` is the string `"START"` or the string `"END"`.
## Frame identity
A *frame* is identified by three things together: its depth in the stack, its function name, and the exact sequence of ancestors above it. `b` at depth 1 under `a` and `b` at depth 1 under `c` are two different frames, and so are `b` at depth 1 and `b` at depth 2. A frame is **present** in a sample when that sample's stack is at least that deep and its prefix, up to and including that depth, equals the frame's own prefix. Otherwise the frame is **absent**.
## Debouncing
`n` is a debounce threshold, and it is applied to each frame independently -- never to the complete stack.
- An inactive frame becomes active once it has been present in `n` consecutive samples. Its `START` carries the timestamp of the **first** sample of that run, even though the run is only confirmed at the last sample of the run.
- An active frame becomes inactive once it has been absent from `n` consecutive samples. Its `END` carries the timestamp of the **first** sample of that absence run, again even though the run is only confirmed later.
- A presence or absence run shorter than `n` is transient: it emits nothing, and it resets the opposing counter.
- One run can confirm several frames at once. If a stack `[a, b]` first appears and then persists for `n` samples, `a` and `b` both start at that first sample's timestamp.
- Churn deep in the stack must not reset an unchanged outer frame's counter, because the outer frame's identity does not mention anything below it.
- A frame can only be present when every one of its ancestors is present, so no frame ever outlives an ancestor.
- With `n = 1` this reduces to comparing each stack with the previous one: the functions dropped from the common prefix end, and the functions added past the common prefix start. A function still inside the common prefix emits nothing.
## Output order
Walk the samples in order. Every event confirmed while processing one sample carries one and the same timestamp; emit that sample's `END` events first, deepest frame first, and then its `START` events, outermost frame first. After the last sample has been processed, append one `END` for every frame that is still active, deepest frame first, all stamped with the **last sample's** timestamp.
## Examples
**Example 1.** `n = 1`, `samples = [(10, ["A", "B"]), (20, ["A", "C"]), (30, [])]`
Output:
```text
[[10, "START", "A"], [10, "START", "B"],
[20, "END", "B"], [20, "START", "C"],
[30, "END", "C"], [30, "END", "A"]]
```
`A` is untouched by the second sample because it stays in the common prefix. At timestamp 30 the stack empties, so both surviving frames end there, deepest first.
**Example 2.** `n = 2`, `samples = [(10, ["A", "B"]), (20, ["A", "C"]), (30, ["A", "C"]), (40, []), (50, [])]`
Output:
```text
[[10, "START", "A"], [20, "START", "C"],
[40, "END", "C"], [40, "END", "A"]]
```
`A` is present in the first three samples, so it starts at 10 -- the churn beneath it is irrelevant. `B` appears once only and is transient, so it never becomes active. `C` is present at 20 and 30, so it starts at 20. Both active frames are absent at 40 and 50, so both end at 40, deepest first.
Constraints
- 1 <= n <= len(samples) <= 100000
- Timestamps are integers and strictly increasing across samples
- -10**15 <= timestamp <= 10**15
- 0 <= len(stack) <= 1000 (an empty stack is allowed)
- Function names are non-empty and unique within a single stack
- 1 <= len(function name) <= 64, made of ASCII letters, digits and underscores
- The stack in force before the first sample is empty
Examples
Input: ([(10, ['A', 'B']), (20, ['A', 'C']), (30, [])], 1)
Expected Output: [[10, 'START', 'A'], [10, 'START', 'B'], [20, 'END', 'B'], [20, 'START', 'C'], [30, 'END', 'C'], [30, 'END', 'A']]
Input: ([(10, ['A', 'B']), (20, ['A', 'C']), (30, ['A', 'C']), (40, []), (50, [])], 2)
Expected Output: [[10, 'START', 'A'], [20, 'START', 'C'], [40, 'END', 'C'], [40, 'END', 'A']]
Hints
- Two adjacent samples share a common stack prefix. Everything inside that prefix is the same frame it was a sample ago; everything past it is either gone or brand new. That single observation drives both the n = 1 rule and the general per-frame counters.
- Keep one counter per stack depth rather than one per function name. The counter at depth d only resets when the stack prefix up to depth d changes -- which is exactly why churn below an outer frame leaves that outer frame alone.
- The timestamp an event carries is not the timestamp at which you confirm it. Record the first timestamp of a run when the run begins and read it back n samples later; the same applies to absence runs.