From ff27a3a235bb52318416768264feaee64484c4d4 Mon Sep 17 00:00:00 2001 From: Navneet Date: Sat, 1 Aug 2026 19:46:21 +0530 Subject: [PATCH] feat(hireup): add Service Call Profiler as OA question A A real OA needs more than two problems, and the set jumped straight to a prefix-imbalance argument. Service Call Profiler is a stack simulation that gives candidates a reachable opening problem. The OA is now A (EASY, stack) < B (MEDIUM, prefix sums) < C (HARD, tree rerooting). Stress tests cover 50k nested re-entries, which breaks recursive walks and any per-entry stack rescan, plus flat call logs across 2 and 100 services. Output stays a few hundred bytes, well inside the judge limit. --- prisma/hireup-problem-set.js | 117 +++++++++++++++--- prisma/seed.mjs | 2 +- .../service-call-profiler.cpp | 45 +++++++ .../service-call-profiler.py | 32 +++++ 4 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 scripts/reference-solutions/service-call-profiler.cpp create mode 100644 scripts/reference-solutions/service-call-profiler.py diff --git a/prisma/hireup-problem-set.js b/prisma/hireup-problem-set.js index ba32bc7..73c44db 100644 --- a/prisma/hireup-problem-set.js +++ b/prisma/hireup-problem-set.js @@ -1,7 +1,6 @@ // Problems for the "HireUp Online Assessment" — the first round of the HireUp -// mock-hiring event. The pair escalates from a prefix-imbalance observation to -// weighted tree rerooting, giving candidates an approachable entry point and a -// stronger second problem within the assessment window. +// mock-hiring event. The set escalates across three problems: a stack-based +// profiler warm-up, a prefix-imbalance observation, and weighted tree rerooting. // // Reference solutions live in scripts/reference-solutions/.{py,cpp} and are // attached by prisma/seed.mjs. Stress-test outputs below are closed-form and @@ -25,7 +24,69 @@ function weightedChain(nodes, weight) { return `${lines.join("\n")}\n`; } -// --- Problem A: Driver Rebalancing Across City Corridors --------------------- +// Profiler log where service 0 re-enters itself `depth` times before unwinding. +// Every time unit in [0, 2 * depth) belongs to service 0, so its exclusive time +// is exactly 2 * depth. +function nestedCallLog(depth) { + const lines = [`1 ${depth * 2}`]; + for (let index = 0; index < depth; index += 1) { + lines.push(`0:start:${index}`); + } + for (let index = 0; index < depth; index += 1) { + lines.push(`0:end:${depth + index}`); + } + return `${lines.join("\n")}\n`; +} + +// Profiler log of back-to-back (never nested) calls cycling through services. +// Each call covers exactly two time units, so a service called c times scores 2c. +function sequentialCallLog(services, calls) { + const lines = [`${services} ${calls * 2}`]; + for (let index = 0; index < calls; index += 1) { + const serviceId = index % services; + lines.push(`${serviceId}:start:${2 * index}`); + lines.push(`${serviceId}:end:${2 * index + 1}`); + } + return `${lines.join("\n")}\n`; +} + +// --- Problem A: Service Call Profiler ---------------------------------------- +const serviceCallProfiler = { + slug: "service-call-profiler", + title: "Service Call Profiler", + statement: + 'Uber runs a profiler over a backend that executes on a single thread. A service can call other services, and a call may even re-enter the same service, so calls nest like a call stack: when one service starts, whichever service was running is paused and resumes only once the inner call finishes.\n\nYou are given the profiler log in execution order. Each entry has the form ":start:" or ":end:".\n\n- ":start:" means service id began running at the very start of time unit t.\n- ":end:" means service id finished at the very end of time unit t, so it was still running for the whole of time unit t.\n\nTimestamps are non-decreasing, every start has a matching end, and the log is consistent with a single thread.\n\nReport the exclusive time of every service: how many time units it spent running itself, not counting time while it was paused waiting on a nested call.\n\nInput format:\n- First line: two integers N and M, the number of services and the number of log entries\n- Next M lines: one log entry each\n\nPrint N space-separated integers, where the value at index i is the exclusive time of service i. The answer can exceed a 32-bit integer.', + constraints: "1 <= N <= 100\n2 <= M <= 10^5, and M is even\n0 <= id < N\n0 <= timestamp <= 10^9", + tags: ["Stack", "Simulation", "String", "Uber", "HireUp"], + difficulty: "EASY", + timeLimitMs: 2000, + samples: [ + { input: "2 4\n0:start:0\n1:start:2\n1:end:5\n0:end:6\n", expectedOutput: "3 4\n" }, + { + input: "1 6\n0:start:0\n0:start:2\n0:end:5\n0:start:6\n0:end:6\n0:end:7\n", + expectedOutput: "8\n", + }, + ], + hidden: [ + { input: "1 2\n0:start:0\n0:end:0\n", expectedOutput: "1\n" }, + { + input: "3 6\n0:start:0\n1:start:3\n2:start:5\n2:end:8\n1:end:10\n0:end:12\n", + expectedOutput: "5 4 4\n", + }, + { input: "2 4\n0:start:0\n0:end:4\n1:start:5\n1:end:9\n", expectedOutput: "5 5\n" }, + { + input: + "2 8\n0:start:0\n1:start:1\n1:end:2\n0:start:3\n0:end:4\n1:start:5\n1:end:6\n0:end:7\n", + expectedOutput: "4 4\n", + }, + { + input: "2 4\n0:start:0\n1:start:500000000\n1:end:999999999\n0:end:1000000000\n", + expectedOutput: "500000001 500000000\n", + }, + ], +}; + +// --- Problem B: Driver Rebalancing Across City Corridors --------------------- const driverRebalancing = { slug: "driver-rebalancing", title: "Driver Rebalancing Across City Corridors", @@ -48,7 +109,7 @@ const driverRebalancing = { ], }; -// --- Problem B: Best Dispatch Hub --------------------------------------------- +// --- Problem C: Best Dispatch Hub --------------------------------------------- const bestDispatchHub = { slug: "best-dispatch-hub", title: "Best Dispatch Hub", @@ -82,29 +143,50 @@ function buildHireupStressTests() { // otherwise a correct solution would have its output truncated and be marked // wrong. At 50k nodes an O(N^2) solution still times out comfortably. const treeN = 50_000; + const logCalls = 50_000; + + // A: 50k nested re-entries of one service. This punishes a recursive walk + // (stack overflow) and any solution that rescans the whole call stack per + // entry, which degrades to O(M^2); the intended single pass stays linear. + const aDeepNesting = { + input: nestedCallLog(logCalls), + expectedOutput: `${2 * logCalls}\n`, + }; + + // A: 50k back-to-back calls alternating between two services, two units each. + const aFlatSequential = { + input: sequentialCallLog(2, logCalls), + expectedOutput: `${logCalls} ${logCalls}\n`, + }; + + // A: the same volume spread across 100 services, so each is called 500 times. + const aManyServices = { + input: sequentialCallLog(100, logCalls), + expectedOutput: repeatValues(100, (2 * logCalls) / 100), + }; - // A: moving one large group from the first zone to the last makes every driver + // B: moving one large group from the first zone to the last makes every driver // cross every boundary. - const aLongTransfer = { + const bLongTransfer = { input: `${bigN}\n100000000 ${repeatValues(bigN - 1, 0)}${repeatValues(bigN - 1, 0).trim()} 100000000\n`, expectedOutput: `${(bigN - 1) * 100_000_000}\n`, }; - // A: each adjacent pair starts with its driver in the left zone and needs it + // B: each adjacent pair starts with its driver in the left zone and needs it // in the right zone, so exactly half the boundaries carry one driver. - const aAlternating = { + const bAlternating = { input: `${bigN}\n${sequenceLine(bigN, (index) => (index % 2 === 0 ? 1 : 0))}${sequenceLine(bigN, (index) => (index % 2 === 0 ? 0 : 1))}`, expectedOutput: `${bigN / 2}\n`, }; - const aAlreadyBalanced = { + const bAlreadyBalanced = { input: `${bigN}\n${repeatValues(bigN, 1000000)}${repeatValues(bigN, 1000000)}`, expectedOutput: "0\n", }; - // B: a long chain forces linear-time traversal and reroot propagation. For a + // C: a long chain forces linear-time traversal and reroot propagation. For a // node r, the sum is 1 + ... + r plus 1 + ... + (N-1-r). - const bChain = { + const cChain = { input: weightedChain(treeN, 1), expectedOutput: sequenceLine(treeN, (root) => { const left = (root * (root + 1)) / 2; @@ -114,20 +196,21 @@ function buildHireupStressTests() { }), }; - // B: in a unit-weight star the center costs N-1 and every leaf costs 2N-3. + // C: in a unit-weight star the center costs N-1 and every leaf costs 2N-3. const starEdges = Array.from({ length: treeN - 1 }, (_, index) => `0 ${index + 1} 1`).join("\n"); - const bStar = { + const cStar = { input: `${treeN}\n${starEdges}\n`, expectedOutput: `${treeN - 1} ${repeatValues(treeN - 1, 2 * treeN - 3)}`, }; return { - "driver-rebalancing": [aLongTransfer, aAlternating, aAlreadyBalanced], - "best-dispatch-hub": [bChain, bStar], + "service-call-profiler": [aDeepNesting, aFlatSequential, aManyServices], + "driver-rebalancing": [bLongTransfer, bAlternating, bAlreadyBalanced], + "best-dispatch-hub": [cChain, cStar], }; } -const hireupProblems = [driverRebalancing, bestDispatchHub]; +const hireupProblems = [serviceCallProfiler, driverRebalancing, bestDispatchHub]; module.exports = { HIREUP_OA_SLUG, diff --git a/prisma/seed.mjs b/prisma/seed.mjs index a812afd..da88d9f 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -1039,7 +1039,7 @@ async function main() { }); // HireUp Online Assessment: the first round of the HireUp mock-hiring event. - // Two escalating problems (A < B) in the style of recent Uber/Amazon/Google + // Three escalating problems (A < B < C) in the style of recent Uber/Amazon/Google // OAs. Seeded unpublished so they only appear inside the OA contest, with // reference solutions for admins. const hireupStress = buildHireupStressTests(); diff --git a/scripts/reference-solutions/service-call-profiler.cpp b/scripts/reference-solutions/service-call-profiler.cpp new file mode 100644 index 0000000..570d664 --- /dev/null +++ b/scripts/reference-solutions/service-call-profiler.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +using namespace std; + +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + + int services, entries; + cin >> services >> entries; + + vector exclusive(services, 0); + vector callStack; + callStack.reserve(entries); + long long previous = 0; + string entry; + + for (int index = 0; index < entries; ++index) { + cin >> entry; + const size_t firstColon = entry.find(':'); + const size_t lastColon = entry.rfind(':'); + const int serviceId = stoi(entry.substr(0, firstColon)); + const long long timestamp = stoll(entry.substr(lastColon + 1)); + + if (entry[firstColon + 1] == 's') { + // The caller pauses here, so it only owns the units before this start. + if (!callStack.empty()) exclusive[callStack.back()] += timestamp - previous; + callStack.push_back(serviceId); + previous = timestamp; + } else { + // An end timestamp is inclusive, hence the +1. + exclusive[callStack.back()] += timestamp - previous + 1; + callStack.pop_back(); + previous = timestamp + 1; + } + } + + for (int index = 0; index < services; ++index) { + if (index > 0) cout << ' '; + cout << exclusive[index]; + } + cout << '\n'; + return 0; +} diff --git a/scripts/reference-solutions/service-call-profiler.py b/scripts/reference-solutions/service-call-profiler.py new file mode 100644 index 0000000..709fd56 --- /dev/null +++ b/scripts/reference-solutions/service-call-profiler.py @@ -0,0 +1,32 @@ +import sys + + +def main(): + data = sys.stdin.buffer.read().split() + services = int(data[0]) + entries = int(data[1]) + + exclusive = [0] * services + call_stack = [] + previous = 0 + + for index in range(entries): + raw_id, action, raw_timestamp = data[2 + index].split(b":") + service_id = int(raw_id) + timestamp = int(raw_timestamp) + + if action == b"start": + # The caller pauses here, so it only owns the units before this start. + if call_stack: + exclusive[call_stack[-1]] += timestamp - previous + call_stack.append(service_id) + previous = timestamp + else: + # An end timestamp is inclusive, hence the +1. + exclusive[call_stack.pop()] += timestamp - previous + 1 + previous = timestamp + 1 + + print(*exclusive) + + +main()