Two real cases in favor of a timeline profiler: multi-camera GPU inference and a WebGPU pipeline in Chrome, where the bottleneck lived below the code the team owned.

When the code-level view stops explaining the delay, the bottleneck is usually not gone. It has just moved into a layer your profiler cannot see.

1. Performance Problems Often Move Below the Application Layer


Performance debugging usually begins where developers have the most control: the application code. A model may meet its target in a local benchmark, a function-level profiler may show no clear slow function, and the first round of configuration changes may look reasonable.

At that point, the problem can feel unclear rather than obviously technical.

The reason is that AI performance depends on more than the code around the model. Runtime backends, GPU drivers, memory transfers, browser processes, hardware contention, and scheduling decisions can all affect latency. These layers are easy to miss when the only view is a list of functions and execution times.

This is where a timeline profiler becomes useful. Instead of asking only “which function took the longest?”, they show when work happened, where it waited, and how different parts of the system overlapped or blocked each other.

This post walks through two real cases investigated by our team at Deep Vision Consulting, where that timeline view changed the diagnosis: one involving GPU inference with Nsight Systems, and one involving browser-based execution with Perfetto.

Different tools expose different layers of the performance stack: a line-level profiler stays close to the code, while a timeline profiler reveals the runtime, hardware, and system coordination around it.

2. Choosing a Profiler Means Choosing the Layer You Need to Observe


A profiler is only useful when its visibility matches the question. line_profiler, for example, is a Python profiler that reports how much time each executed line of selected Python functions takes. It is the right reach when a hot loop, a slow data transformation, or an unexpected algorithmic cost lives in the code you wrote. viztracer extends that view into a timeline and call graph, which can make Python execution easier to reason about over time.

But many AI deployment problems are not line-level Python problems. Once work moves into ONNX Runtime, CUDA, TensorRT, WebGPU, Chrome, or a native driver, Python profilers can still tell you where control was handed off, but not what the downstream system did with it.

That is where a timeline profiler becomes more valuable. NVIDIA Nsight Systems shows CPU threads, CUDA API calls, GPU kernels, memory transfers, and synchronization on a unified NVIDIA-platform timeline.

Perfetto shows cross-process trace events across systems such as Chrome, where renderer processes, GPU processes, IPC, and driver activity interact. They often answer a similar high-level question: what was really happening below the application?

This matters because choosing the wrong profiler can make a real bottleneck invisible. A precise number from the wrong layer is still the wrong evidence.

3. Case 1: When the Inference Backend Sets the Limit


The first case came from an industrial computer vision system with a production throughput target in the mid-80 FPS range per camera. Two camera streams ran on the same hardware, each in its own application process, while neural network inference was executed through ONNX Runtime on a single shared NVIDIA GPU.

That shared GPU was the central constraint. Because the system used pipelining across CPU cores, the full processing path did not necessarily need to fit inside one frame interval. What mattered was whether GPU-side inference, data movement, and runtime scheduling could keep up with the combined workload without starving the rest of the system of CPU headroom.

This made a timeline view essential. A function-level profiler could show how long the application code took, but it could not explain how the runtime submitted work, when the GPU executed it, or whether the two processes were competing for the same device in a way that limited throughput.

The system initially used the CUDA Execution Provider in ONNX Runtime. An Execution Provider is the backend that decides how ONNX Runtime maps and runs a model onto available hardware. On NVIDIA hardware, CUDA EP is the standard general-purpose GPU backend, while TensorRT EP is a more specialized optimizing backend that can build device-specific engines for faster inference.

Before going deeper, the team exhausted the reasonable configuration-level options. Graph optimization settings, execution mode options, and batching variants were all tested. The most informative negative result was batching: inference time increased approximately linearly with batch size. That did not prove a universal law about the GPU, but it did tell us something practical about this model, this device, and this backend: batching was not exposing useful parallelism or amortizing overhead. The next lever had to be how a single inference was executed on the GPU. At that point, NVIDIA Nsight Systems became the right tool. Nsight Systems is a system-wide timeline profiler for NVIDIA platforms. Instead of only reporting where application code spends time, it places CPU activity, CUDA API calls, GPU kernels, memory transfers, and synchronization events on the same timeline. For a multi-camera, multi-process deployment, that unified view was essential.

The profile showed that the application pipeline was not the primary explanation. Host-to-device transfer overlapped reasonably with compute, but the CUDA timeline exposed costly kernel regions and idle gaps where the GPU was not doing useful work. Instead of pointing to one suspicious line of application code, the evidence shifted attention to the runtime/backend layer as the most plausible place to recover time.

The backend change did not remove GPU work; it changed how that work was scheduled and executed, reducing idle gaps and making inference fit the deployment budget.

This is also why the backend was not changed blindly at the start. Switching from CUDA EP to TensorRT EP can look like “just a flag,” but in production it changes the deployment profile. TensorRT engines are tied to model, runtime, precision, optimization profile, and GPU characteristics. Without profiling, the team would have been swapping architectural risk into a production system without evidence that the risk addressed the actual bottleneck.

The Nsight timeline made the trade-off worth testing. TensorRT EP was a good fit because the evidence pointed to kernel selection and execution behavior, and TensorRT’s engine build process is designed to search for faster implementations on the target GPU. After switching, the real system moved into the right performance range: representative single-inference measurements dropped from roughly the low-20 millisecond range under the CUDA setup to roughly the 5–8 millisecond range under the TensorRT setup, depending on contention and measurement point.

The backend change created a second problem: startup became long and occasionally unstable. Nsight was useful again, this time not for inference latency but for startup diagnosis. The timeline showed repeated TensorRT engine-building work on the GPU, performed across multiple sessions in sequence. This was consistent with how TensorRT EP works: during engine construction, it profiles candidate kernels and builds an optimized engine for the model and device. The fix was not another round of application refactoring. It was caching. In this deployment, enabling timing and engine caching preserved the expensive build work across runs, so startup no longer repeated the same kernel-selection process every time. The startup instability disappeared, and the two camera applications could run together within the required throughput envelope.

Finally, Nsight helped characterize robustness rather than raw speed. The team used it to observe behavior under different contention patterns: workers hitting the GPU at the same time versus workers alternating. The exact latency of an individual inference mattered less than the bounded behavior of the full system. Under contention, GPU work serializes; under alternation, individual inferences stay shorter. The value of profiling was to measure both regimes clearly enough for the deployment team to accept the system, not to pretend there was a single magic latency number. This matters because industrial AI systems are judged by sustained operation. A backend change that improves a benchmark but destabilizes startup or fails under concurrency is not a production win. In this case, Nsight Systems provided the evidence for the change, the diagnosis for the side effect, and the robustness data needed to close the loop.

4. Case 2: When the Browser Sets the Limit


The second case came from a web product that needed browser-native GPU inference. The goal was to run on the user’s device, directly in the browser, without a cloud round trip or native installation, while staying close to a real-time 60 FPS interaction target. That combination is commercially attractive: easier distribution, lower infrastructure dependency, better privacy posture, and lower user friction.

The pipeline used WebGPU through ONNX Runtime’s WebGPU backend. WebGPU is the modern web API for GPU compute and rendering. Conceptually, it exposes GPU adapters, devices, queues, buffers, textures, and command submission to web applications, while the browser implementation maps that work onto native graphics APIs such as Direct3D 12, Metal, or Vulkan.

That architecture is exactly why debugging WebGPU performance is not the same as profiling CUDA. With Nsight on a native NVIDIA stack, you can often see CUDA kernels, memory transfers, and device activity with high fidelity. In a browser, WebGPU intentionally abstracts the hardware and mediates access for security, and portability. Any timeline profiler may show process coordination and GPU-track events, but it will not automatically give the same kernel-level view of what a native CUDA profiler would expose.

The reported issue was a latency gap in the in-browser pipeline. The natural starting point was to investigate the application-owned GPU work and the model execution path. For this kind of browser problem, Perfetto was the appropriate tool because Chrome emits trace events across the renderer process, GPU process, and related subsystems. Perfetto could show the timing relationship between those components on a single timeline.

The key finding was initially ambiguous. A measured WebGPU operation took about 3.2 milliseconds wall-clock, yet the hardware execution measured through WebGPU-internal timing was less than 1 millisecond, roughly tens of microseconds in the representative frame. Perfetto showed a submit, then a quiet gap, then a linked completion event. Without the hardware timing, that gap could have been hidden GPU work that the trace simply could not expose. With the hardware timing, the interpretation changed: the GPU had finished quickly, and the missing time was somewhere in coordination back to the browser.

The team then read the Chromium source to understand the mechanism behind the gap. Chrome’s GPU process detects GPU fence completion using timer-based polling rather than immediate event-driven notification. After a submit, Chrome waits before checking whether the fence has completed.

The delay is documented in source comments as intentional: it helps avoid unstable alternating fast/slow frame behavior when multiple command buffer stubs compete for GPU time. In other words, the latency was not a bug in the application shader, a missing model optimization, or a driver mystery. It was a browser scheduling choice made for system-level stability.

That finding changed the optimization plan. The application-owned GPU work was too small to recover the missing frame budget by itself. ONNX inference remained the dominant model-side cost, and the browser polling interval was external to the application. Optimizing the small shader more aggressively would have produced a technically satisfying improvement in a place that did not move the total system enough.

The practical lesson was about synchronization boundaries. If a pipeline submits GPU work, waits, submits another small piece of GPU work, and waits again, it can pay browser coordination costs multiple times.

The GPU finished quickly; the delay came from when the browser observed completion, turning a short execution into a longer wall-clock span.

When possible, the better design is to queue related GPU work as quickly as the architecture allows and wait once, or to overlap unavoidable waiting with CPU work that must happen anyway. The exact refactor depends on the product constraints, but the principle is clear: do not introduce extra GPU/CPU synchronization points unless they buy something worth their cost. This matters because browser AI performance is often limited by orchestration, not just arithmetic. Perfetto did not reveal a slow shader that needed rewriting. It revealed which costs were addressable, which costs were structural, and how future pipeline changes should avoid multiplying the browser’s scheduling overhead.

5. The Pattern Across Both Cases


The two investigations looked different on the surface: one was a native NVIDIA deployment, the other a browser-based WebGPU product. But the working pattern was the same. The first useful question was not “which line is slow?” but “which layer owns the time we are missing?”

In the multi-camera system, application-level tuning had already reached its limit. Nsight Systems showed that the meaningful leverage was in the inference backend and later in TensorRT startup caching. In the WebGPU system, the application-owned GPU work was not the real budget consumer. Perfetto and Chromium source inspection showed that the browser’s GPU coordination model was setting a structural bound. The counterintuitive lesson is that profiling does not always end with a local optimization. Sometimes it justifies an architectural change. Sometimes it prevents one. In both cases, the value of the timeline profiler was the same: it replaced a plausible story with measured evidence.

For engineering teams, that evidence changes prioritization. For executives, it reduces the risk of spending weeks optimizing work that will not move the product metric. For students and future hires, it is a reminder that modern AI systems are stacks: model code, runtime, backend, driver, operating system, browser, and hardware can all own part of the timeline. The right profiler is not the most powerful one in the abstract. It is the one that can see the layer where the system is spending time. line_profiler and viztracer remain excellent when the question lives inside Python. Nsight Systems is the right reach when the question moves into CUDA, GPU timelines, transfers, and synchronization on NVIDIA platforms. Perfetto is the right reach when the question moves into browser or system-level coordination across processes.

If your team is hitting performance targets in development but losing them in production, or seeing behavior that application-level profiling cannot explain, this is the kind of production-level performance investigation we carry out regularly. Reach out at info@deepvisionconsulting.com to discuss your specific situation.


Frequently Asked Questions


To complement the discussion above, here are concise answers to some common questions about the topic, from basic definitions and key distinctions to practical engineering considerations.

A timeline profiler shows when operations occur across time and how different parts of a system interact. Depending on the tool, it can expose CPU threads, GPU kernels, memory transfers, synchronization events and activity across different processes.

A code profiler identifies which functions or lines of application code consume the most time. A timeline profiler instead reveals how work is scheduled, where the system waits and whether CPU, GPU, runtime or system-level operations overlap or block one another. The two approaches are complementary and investigate different layers of the system.

A timeline profiler becomes particularly useful when application-level profiling cannot explain the observed latency or throughput. This often happens in AI systems where execution passes through runtimes, inference backends, GPU drivers, browser processes or other layers outside the application code.

A standalone benchmark normally isolates model inference, while a production pipeline also includes data transfers, synchronization, hardware contention, runtime scheduling and concurrent workloads. As a result, model execution may meet its target in isolation while the complete system still fails to achieve the required throughput.

Related Posts