Taro Logo

Exclusive Time of Functions

Medium
Google logo
Google
1 view
Topics:
StacksArrays

You are given logs describing the execution of n functions on a single-threaded CPU. Each function has a unique ID from 0 to n-1. The logs are in the format "{function_id}:{type}:{timestamp}", where type is either "start" or "end".

For example, the log "0:start:3" means function 0 started at timestamp 3, and "1:end:2" means function 1 ended at timestamp 2. A function's exclusive time is the sum of execution times for all its calls.

Your Task:

Write a function that takes n and a list of logs and returns an array where the value at index i represents the exclusive time for function i.

Example:

n = 2
logs = ["0:start:0","1:start:2","1:end:5","0:end:6"]
output = [3, 4]

Explanation:

  • Function 0 starts at 0, executes for 2 units, and resumes at 6 for 1 unit, totaling 3.
  • Function 1 starts at 2 and executes for 4 units, totaling 4.

Constraints:

  • 1 <= n <= 100
  • 2 <= logs.length <= 500
  • 0 <= function_id < n
  • 0 <= timestamp <= 10^9

How would you approach this problem, and what would be the time and space complexity of your solution?

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What is the range of possible thread IDs, and are they guaranteed to be non-negative integers?
  2. What is the format of the log entries? Specifically, can I assume the format is always "{function_id}:START_OR_END:{timestamp}"?
  3. If a function is still running when the input ends (i.e., there's a START but no corresponding END), how should I calculate its exclusive time?
  4. Can function IDs be non-sequential, or are they guaranteed to be a contiguous range starting from 0?
  5. Can I assume the input logs are chronologically sorted by timestamp, or do I need to handle out-of-order log entries?

Brute Force Solution

Approach

We need to figure out how much time each function spends running, given logs of when they start and stop. The brute force way is like simulating the entire execution, second by second, and tracking which function is running at each moment. It's like watching the whole process unfold.

Here's how the algorithm would work step-by-step:

  1. Go through the logs one by one.
  2. For each second of the execution, check which function is active based on the start and end times.
  3. If a function is running during that second, add one to its running time.
  4. Repeat this process for every single second covered by the logs.
  5. Once you have gone through all the seconds, you'll know the total running time for each function.

Code Implementation

def exclusive_time_brute_force(number_of_functions, logs):
    function_execution_times = [0] * number_of_functions
    current_time = 0

    log_entries = []
    for log in logs:
        function_id, event_type, timestamp = log.split(":")
        log_entries.append((int(function_id), event_type, int(timestamp)))

    while True:
        current_function = -1
        # Find the currently running function.
        for function_id in range(number_of_functions):
            start_found = False
            end_found = False
            start_time = -1
            end_time = -1

            for function_id_log, event_type, timestamp in log_entries:
                if function_id_log == function_id:
                    if event_type == "start" and timestamp <= current_time:
                        start_found = True
                        start_time = timestamp
                    if event_type == "end" and timestamp >= current_time:
                        end_found = True
                        end_time = timestamp

            if start_found and not end_found:
                current_function = function_id
                break

        if current_function != -1:
            function_execution_times[current_function] += 1

        # Check if all logs have been processed to stop the simulation.
        all_logs_processed = True
        max_time = -1
        for function_id, event_type, timestamp in log_entries:
            max_time = max(max_time, timestamp)
        if current_time > max_time:
            all_logs_processed = True
        else:
            all_logs_processed = False

        if all_logs_processed and current_function == -1:
            break

        current_time += 1

    return function_execution_times

Big(O) Analysis

Time Complexity
O(m*n)The algorithm iterates through each log entry (n log entries). For each log entry, it simulates the execution second by second. The total time span of the execution is determined by the difference between the last and first timestamps in the logs, which we can represent as m. For each of the n log entries we may iterate up to m times. Therefore the total runtime is proportional to m*n.
Space Complexity
O(1)The brute force solution, as described, doesn't use any auxiliary data structures that scale with the number of logs. It iterates through each second of execution and increments the running time for the currently active function directly; it does not store or accumulate intermediate results in auxiliary data structures. It keeps track of the current function, which takes constant space. Thus, the space used remains constant regardless of the input size N (number of logs), making the auxiliary space complexity O(1).

Optimal Solution

Approach

To efficiently calculate the exclusive time of functions, we can use a stack to track the currently executing function. When a function starts, we push it onto the stack, and when it ends, we pop it. The exclusive time is calculated by tracking the durations between start and end logs, taking into account nested function calls.

Here's how the algorithm would work step-by-step:

  1. We'll use a container to keep track of which function is currently running.
  2. When we see a function starting, we record the exact moment it begins.
  3. Before a new function begins, if we have a function currently running, we calculate how long it has been running so far and add it to that function's total running time.
  4. When a function ends, we calculate how long it ran and add that to its total running time.
  5. Remember to update the start time whenever a new function starts, or a function ends and we resume the function that was previously running.
  6. By the end, we'll have the total exclusive running time for each function.

Code Implementation

def get_exclusive_time(number_of_functions, logs):
    exclusive_times = [0] * number_of_functions
    stack_of_functions = []
    previous_time = 0

    for log in logs:
        function_id, operation, timestamp = log.split(':')
        function_id = int(function_id)
        timestamp = int(timestamp)

        if operation == 'start':
            # Account for time elapsed in the previous function
            if stack_of_functions:
                exclusive_times[stack_of_functions[-1]] += timestamp - previous_time

            stack_of_functions.append(function_id)
            previous_time = timestamp

        else:
            # Accumulate time for current function and update the time
            exclusive_times[function_id] += timestamp - previous_time + 1

            stack_of_functions.pop()
            previous_time = timestamp + 1
            # Update previous_time after end

    return exclusive_times

Big(O) Analysis

Time Complexity
O(n)The algorithm iterates through the log entries once, where n is the number of logs. Within the loop, stack operations (push and pop) take constant time O(1). Calculating time differences and updating function times also take O(1) time. Since the dominant operation is iterating through the logs once, the overall time complexity is O(n).
Space Complexity
O(N)The algorithm uses a stack to keep track of the currently running function. In the worst-case scenario, all function calls could be nested, meaning that N function IDs could be pushed onto the stack, where N is the number of logs. This creates an auxiliary data structure (the stack) whose memory consumption grows linearly with the number of log entries. The exclusive times of the functions are stored in a list whose maximum size could also be N. Therefore, the space complexity is O(N).

Edge Cases

CaseHow to Handle
Null or empty logs arrayReturn an empty list or an array of zeros of appropriate size to indicate no execution time.
Logs array with only 'start' events and no corresponding 'end' events.Handle incomplete function calls by ignoring the 'start' events at the end or assuming they end at the end of the entire log.
Logs array with only 'end' events and no corresponding 'start' events.Ignore such 'end' events since they are invalid without a matching 'start'.
Nested function calls that overlap (e.g., function A starts, then function B starts, then function A ends, then function B ends).The stack-based approach should correctly handle overlapping calls by assigning exclusive time based on the current top of the stack.
Number of functions (n) is very large, potentially leading to large result array allocation.Ensure memory is allocated dynamically and efficiently; consider a more memory-efficient data structure if scaling becomes a problem.
Log timestamps are not strictly increasing.Sort the log entries by timestamp before processing to ensure correct time calculation, or throw an error if non-increasing logs are considered invalid.
Integer overflow in calculating execution time differences.Use long integers or appropriate data types to avoid integer overflow when calculating differences in timestamps.
Function calls that start and end at the same timestamp.Handle zero-duration calls correctly by ensuring the time difference calculation is accurate (end_time - start_time + 1 when appropriate).