🏠

Part III: Methodology & Workflows

7.7 The Execution Tracing Workflow

You've just joined a team maintaining a Django application with 40,000 lines of code spread across twelve apps. Your first task: "The password reset email isn't being sent. Figure out why." You open the codebase and stare at views.py, forms.py, tasks.py, and a mysterious signals.py file. Where do you even start?

This is where most developers make the same mistake: they start reading code from what they think is the entry point, jumping between files, trying to build a mental model through static analysis alone. Three hours later, they're drowning in possibilities, unsure which code paths actually execute.

The execution tracing workflow we're about to explore solves this problem systematically. It's not a random collection of techniques—it's a deliberate, three-phase process that professional developers use to understand unfamiliar code in days, not weeks. The key insight: you must observe what the code actually does before you can understand what it's supposed to do.

This workflow assumes you have the tools from Part II already installed. If you haven't set up Django Debug Toolbar, VS Code debugger, or the equivalent tools for your stack, pause here and complete that setup. The workflow is tool-dependent by design—because tools are what make this approach feasible.

7.7.1 Phase 1: High-Level Mapping (Day 1)

Phase 1 answers one question: "What major components are involved when I do X?" Not how they work, not why they're designed that way—just what actually runs. This phase typically takes 2-4 hours and gives you the execution landscape.

The Problem This Solves: Jumping straight into debugger breakpoints without high-level context is like trying to navigate a city by examining individual streets under a microscope. You need the map first, the street-level detail second. Phase 1 builds that map.

Let's walk through this with a real scenario: understanding how a Django application handles a form submission that creates a new user account.

Step 1: Install Framework-Specific Tools

Start with the tools that give you the most information with the least effort. For Django, that's Django Debug Toolbar. For React, it's React DevTools. For FastAPI, it's the built-in /docs endpoint plus careful logging configuration.

Here's what installing Django Debug Toolbar actually looks like in practice:

# In your virtual environment

pip install django-debug-toolbar
# settings.py

INSTALLED_APPS = [

    # ... other apps

    'debug_toolbar',

]



MIDDLEWARE = [

    'debug_toolbar.middleware.DebugToolbarMiddleware',

    # ... other middleware

]



INTERNAL_IPS = [

    '127.0.0.1',

]
# urls.py

from django.urls import path, include



urlpatterns = [

    # ... your patterns

    path('__debug__/', include('debug_toolbar.urls')),

]

This five-minute setup is worth emphasizing: you're not building instrumentation, you're installing a tool that already knows how Django works. The toolbar understands middleware chains, ORM queries, template rendering, and signal dispatching. It took the Django community years to build this knowledge into the tool. Use it.

Step 2: Perform the Target Action

Now do the thing you're trying to understand, but do it deliberately while watching the Debug Toolbar. In our user registration example:

  1. Start your Django development server: python manage.py runserver

  2. Open your browser to the registration page

  3. Open the Debug Toolbar (it appears as a sidebar automatically)

  4. Fill out the registration form and submit it

  5. Stop and observe the toolbar before doing anything else

This is the moment where developers often rush ahead. Don't. The toolbar is showing you the execution summary—read it carefully.

What you're looking for:

For our registration example, you might see:

SQL Queries: 8

Time: 245ms

View: accounts.views.register_user

Signals: 3 fired (user_post_save, profile_created, email_queued)

This tells you immediately: "This isn't just creating a User object—there are signals doing additional work, and something is interacting with an email queue."

This Is Crucial: You now know there are at least three different systems involved (user creation, profile management, email handling) without reading a single line of code. That's the power of observing execution rather than inferring it from code.

Step 3: Identify Entry Points and Major Components

Now translate what the Debug Toolbar showed you into actionable knowledge. Create a simple list:

Entry Point:

  - URL: /accounts/register/

  - View: accounts.views.register_user

  - Method: POST



Major Components Involved:

  - accounts.forms.RegistrationForm (form validation)

  - django.contrib.auth.models.User (user creation)

  - accounts.models.UserProfile (profile creation via signal)

  - accounts.tasks.send_welcome_email (async email task)



Key Observations:

  - 3 signals fire (check accounts/signals.py)

  - 8 database queries (potential optimization opportunity)

  - Email is queued, not sent immediately (Celery task?)

Notice what this list contains: concrete file and function names, not abstractions. You're not writing "the system processes user data"—you're writing "accounts.views.register_user handles the POST request."

For JavaScript/React applications, the process is similar but uses different tools. Let's say you're tracing a button click that updates a cart:

  1. Open React DevTools in Chrome

  2. Click the "Add to Cart" button

  3. Watch the Components tab to see which components re-render

  4. Check the Profiler tab to see the render cascade

  5. Open the Sources tab in Chrome DevTools and check the Network tab for API calls

You might document:

Entry Point:

  - Component: CartButton (src/components/CartButton.jsx)

  - Click handler: handleAddToCart



Major Components Involved:

  - CartButton (triggers action)

  - Redux action: addToCart (src/store/actions/cart.js)

  - Redux reducer: cartReducer (updates state)

  - API call: POST /api/cart/items

  - Components that re-render: CartButton, CartIcon, CartTotal



Key Observations:

  - Entire cart subtree re-renders (24 components)

  - API call takes 120ms

  - CartIcon re-renders unnecessarily (not using React.memo)

Step 4: Sketch the Execution Flow Diagram

This is where many developers skip ahead, assuming they can keep the flow in their heads. Don't make that mistake. Drawing the flow—even crudely—externalizes your understanding and reveals gaps immediately.

Your diagram doesn't need to be formal UML. A simple flowchart or even a text-based list with indentation works perfectly:

User submits registration form

  

Django receives POST to /accounts/register/

  

Middleware chain processes request

  

accounts.views.register_user executes

  

  ├─→ Form validation (accounts.forms.RegistrationForm)

       └─→ Check email uniqueness (DB query #1)

  

  └─→ Form valid: proceed with user creation

        

        User.objects.create() (DB query #2)

        

        SIGNAL: django.contrib.auth.signals.user_post_save fires

        

        ├─→ accounts.signals.create_user_profile

             └─→ UserProfile.objects.create() (DB query #3)

        

        ├─→ accounts.signals.send_welcome_email

             └─→ Queues Celery task: tasks.send_welcome_email

                   └─→ Task stored in Redis (not executed yet)

        

        └─→ accounts.signals.log_registration

              └─→ Audit log entry (DB query #4)

        

  Redirect to /accounts/welcome/

This diagram took maybe 10 minutes to create, and it's worth its weight in gold. Here's why:

  1. It reveals the signal chain that you'd never find by reading the view function alone

  2. It shows asynchronous boundaries (the Celery task queuing)

  3. It maps DB queries to specific operations (now you know where those 8 queries come from)

  4. It identifies where to set breakpoints in Phase 2 (you'll want them in create_user_profile and send_welcome_email)

The Phase 1 Output

At the end of Day 1, you should have:

What you should NOT have yet:

This is the key insight of Phase 1: You're building the map, not exploring the territory. Resist the temptation to dive deep into any single component. You'll do that in Phase 2, but only after you understand the landscape.

Common Phase 1 Mistakes

Mistake 1: Diving too deep too early. You see send_welcome_email in the flow and immediately open the file to understand how it works. Stop. That's Phase 2 work. In Phase 1, you just need to know that it executes, not how it works.

Mistake 2: Skipping the diagram. You feel like you understand the flow and don't need to draw it. This is overconfidence. The act of diagramming forces you to confront gaps in understanding. If you can't draw it, you don't understand it well enough yet.

Mistake 3: Not using framework tools. You think "I'll just trace through the code manually." This takes 10x longer and misses things like signals, middleware, and async boundaries that are invisible in static code.

Mistake 4: Trying to understand everything at once. Phase 1 is deliberately shallow and broad. You're scanning, not studying. Deep understanding comes later.

Phase 1 for Different Frameworks

The process adapts to your framework but the principles remain:

Flask: Flask-DebugToolbar gives similar insights to Django Debug Toolbar. The execution flow is often simpler (fewer signals, simpler middleware), but the process is identical: observe, list components, diagram flow.

FastAPI: Use the auto-generated /docs endpoint to understand request/response schemas, then add strategic print() statements (yes, in Phase 1, print() is actually useful for async function boundaries) or use uvicorn --log-level debug to see execution order.

React/Vue: DevTools show component hierarchy and render cascades. Start by identifying the root component that triggers the behavior, then trace down through the component tree. Pay special attention to state updates and which components re-render.

Node.js/Express: Similar to Flask—simpler middleware chains, more explicit routing. Use morgan logging middleware in dev mode to see request flow, then trace through route handlers.

The Phase 1 Time Box

Give yourself a maximum of 4 hours for Phase 1. If you're still discovering major new components after 4 hours, you're either:

  1. Working with an extremely complex feature (rare)

  2. Diving too deep (common)

  3. Not using the right tools (very common)

If you hit the 4-hour mark, stop and review. Do you have a diagram? Have you listed the major components? If yes, move to Phase 2. If no, you're probably stuck in a debugging mindset instead of a mapping mindset. Reset and try again with deliberate shallowness.

7.7.2 Phase 2: Deep Dive (Days 2-3)

Phase 1 gave you the map. Phase 2 is where you explore the territory. This is when you actually understand how things work, not just what runs. Plan for 8-16 hours of focused work spread over 2-3 days.

The Question Phase 2 Answers: "How does each component actually implement its role in the execution flow, and what data flows between them?"

The Critical Shift: In Phase 1, you avoided reading code in detail. In Phase 2, you embrace it—but strategically. You're not reading random files hoping to understand. You're following the execution path you mapped in Phase 1, using the debugger to show you exactly what happens.

Step 1: Set Strategic Breakpoints at Identified Entry Points

Open your Phase 1 diagram and identify the key decision points and transformations. These are your breakpoint targets. For our user registration example:

# accounts/views.py - Entry point

def register_user(request):

    if request.method == 'POST':

        form = RegistrationForm(request.POST)  # ← BREAKPOINT 1: Form validation starts

        if form.is_valid():

            user = form.save()  # ← BREAKPOINT 2: User creation

            login(request, user)  # ← BREAKPOINT 3: Session creation

            return redirect('welcome')

        else:

            # ... error handling
# accounts/signals.py - Signal handlers

@receiver(post_save, sender=User)

def create_user_profile(sender, instance, created, **kwargs):

    if created:  # ← BREAKPOINT 4: Profile creation logic

        UserProfile.objects.create(user=instance)



@receiver(post_save, sender=User)

def send_welcome_email(sender, instance, created, **kwargs):

    if created:  # ← BREAKPOINT 5: Email queueing

        tasks.send_welcome_email.delay(instance.id)

Notice these breakpoints are at transformation boundaries—places where data changes form or crosses system boundaries:

This is a key pattern: Set breakpoints where data transforms, not just anywhere you're curious. This gives you maximum insight into how the system actually processes information.

How to Actually Set Breakpoints

In VS Code with Python:

  1. Open accounts/views.py

  2. Click in the gutter (left of the line numbers) next to form = RegistrationForm(request.POST)

  3. A red dot appears—that's your breakpoint

  4. Press F5 to start debugging (or use the Debug panel)

  5. VS Code will prompt you to select a configuration—choose "Django"

If your launch.json isn't configured yet:

{
  "version": "0.2.0",

  "configurations": [
    {
      "name": "Django",

      "type": "python",

      "request": "launch",

      "program": "${workspaceFolder}/manage.py",

      "args": ["runserver", "--noreload"],

      "django": true,

      "justMyCode": false
    }
  ]
}

That "justMyCode": false setting is critical—it lets you step into Django's own code, which you'll absolutely need to do.

For JavaScript in Chrome DevTools:

  1. Open Sources tab

  2. Navigate to your file (Cmd/Ctrl+P for quick open)

  3. Click line numbers to set breakpoints

  4. Perform the action that triggers the code

  5. DevTools pauses at your breakpoint

Step 2: Step Through Execution with Debugger

This is where the magic happens. You're about to see exactly what executes, in exactly what order, with actual runtime data visible.

Let's walk through what you see when you hit your first breakpoint:

# You're paused at this line:

form = RegistrationForm(request.POST)

Look at the Call Stack panel in VS Code. You'll see something like:

register_user (accounts/views.py:42)

   get_response (django/core/handlers/base.py:123)

     __call__ (django/core/handlers/wsgi.py:139)

       __call__ (django/contrib/staticfiles/handlers.py:25)

         run (werkzeug/serving.py:852)

Notice This Carefully: The call stack shows you the actual execution path from the web server through Django's WSGI handler to your view. This is information you couldn't get from reading code—you'd never think to look in django/core/handlers/base.py to understand request handling.

Now inspect variables. Hover over request or type request in the Debug Console. You'll see:

<WSGIRequest: POST '/accounts/register/'>

request.POST: <QueryDict: {'username': 'newuser', 'email': 'new@example.com', 'password': 'secret123'}>

request.user: <AnonymousUser>

request.session: <SessionStore object>

This tells you immediately: The user isn't authenticated yet (AnonymousUser), but the session exists. That's a small detail, but it matters when you're debugging authentication flows.

Step Over vs. Step Into

Here's where beginners get lost. Your debugger has several stepping options:

When should you use each?

Step Into when:

Step Over when:

Let's apply this. You're at:

form = RegistrationForm(request.POST)

Should you Step Into the RegistrationForm constructor? Yes, if you don't understand how form validation works. No, if you're confident about forms and just want to see what happens after validation.

Let's say you Step Over. Now you're at:

if form.is_valid():

Inspect form in the debug console:

>>> form.is_valid()

True

>>> form.cleaned_data

{'username': 'newuser', 'email': 'new@example.com', 'password': <hashed>}

>>> form.errors

{}

You can call methods and inspect state in the debug console! This is incredibly powerful. You're not just watching execution—you're exploring runtime state interactively.

Now Step Into form.save(). The debugger jumps to:

# accounts/forms.py

def save(self, commit=True):

    user = super().save(commit=False)

    user.set_password(self.cleaned_data['password'])

    if commit:

        user.save()  # ← You're here now

    return user

See how the debugger took you directly to the relevant code? You didn't need to search for "where does form saving happen"—the debugger showed you.

Continue to the Signal Handlers

Press Continue (F5) to resume. Remember you set a breakpoint in the signal handler. The debugger stops at:

# accounts/signals.py

def create_user_profile(sender, instance, created, **kwargs):

    if created:  # ← Paused here

        UserProfile.objects.create(user=instance)

Check the call stack again:

create_user_profile (accounts/signals.py:12)

   send (django/dispatch/dispatcher.py:178)

     send_robust (django/dispatch/dispatcher.py:205)

       save (django/db/models/base.py:745)

         save (accounts/forms.py:28)

           register_user (accounts/views.py:44)

This Is The Key Insight: Signals fire during user.save(), not after register_user returns. The call stack proves it. If you'd just read the code, you might assume signals fire "after the view finishes." They don't—they fire synchronously during the save operation.

This has huge implications:

You wouldn't discover this from code reading. You needed to see the execution happen.

Step 3: Document Unexpected Discoveries

As you step through execution, you'll discover things that surprised you. Document them immediately. Don't wait until you "finish understanding" to write notes—you'll forget the details.

Update your Phase 1 diagram with annotations:

User.objects.create() (DB query #2)

  ↓

  SIGNAL: post_save fires **DURING save(), not after**

  ↓

  ├─→ create_user_profile

  │     **Runs in same transaction as user creation**

  │     **If this fails, user creation rolls back**

  │     └─→ UserProfile.objects.create() (DB query #3)

Keep a running list of discoveries:

Unexpected Discoveries:

1. Signals fire synchronously during save(), not asynchronously

2. Email task is queued to Redis, not database

3. Form validation makes a DB query to check email uniqueness

4. Login() creates session AFTER signals fire (so signals see AnonymousUser)

5. UserProfile has a default avatar set via model default, not signal

These discoveries matter because they reveal constraints and behaviors you need to respect when modifying the code later.

Step 4: Build Mental Model of Architecture

By the end of Phase 2, you should be able to explain the execution flow to someone else without looking at your notes. This is your mental model.

Test your understanding by writing a summary:

User Registration Architecture:



Django receives POST to accounts/register/. The view creates a

RegistrationForm with POST data. The form validates by checking:

- Required fields present

- Email format valid

- Email unique (DB query to User table)

- Password meets strength requirements



If valid, form.save() creates a User object. During User.save(),

Django fires the post_save signal synchronously (still in the same

transaction). Three signal handlers execute:



1. create_user_profile: Creates UserProfile with default avatar

2. send_welcome_email: Queues Celery task (doesn't send immediately)

3. log_registration: Writes audit log entry



All of this happens atomically. If any signal handler fails, the

entire user creation rolls back.



After user creation succeeds, the view calls login(request, user)

to create an authenticated session, then redirects to /accounts/welcome/.



The actual email sends later when a Celery worker picks up the queued

task from Redis.

If you can write this summary and it's accurate, you understand the system. If you can't, there are gaps in your mental model—set more breakpoints and step through again.

Phase 2 for Different Patterns

Async JavaScript/TypeScript: Stepping through async code requires understanding the event loop. When you hit an await, the debugger will show you the pause, but understand that other code may execute before this async function resumes. Use the Call Stack to see where you came from, and set breakpoints after await statements to see what happens when the promise resolves.

React Component Rendering: Use React DevTools Profiler to record a render cycle, then step through component code with Chrome DevTools. The Profiler shows you which components rendered and why. The Sources tab shows you the actual code execution. Used together, you can trace exactly what prop change triggered which component update.

Background Jobs: Debugging Celery tasks requires connecting a debugger to the worker process. Use remote debugging or pdb.set_trace() in the task code, then trigger the task and watch the worker console. The execution context is different from web requests—there's no HTTP request object, but you do have the Celery task context.

Common Phase 2 Mistakes

Mistake 1: Not using the call stack. You step through code but never look at the Call Stack panel. This means you miss the most important information: what called this code? The call stack reveals architectural patterns you'd never see otherwise.

Mistake 2: Stepping into library code too much. You Step Into every function call and get lost in Django's internals. Learn to recognize library code and Step Over it unless you specifically need to understand it.

Mistake 3: Not inspecting variables. You watch code execute but don't look at runtime values. The debugger is showing you actual data—use it. Hover over variables, check them in the Debug Console, see what they contain.

Mistake 4: Giving up when execution seems "weird". The debugger shows you something that doesn't match your expectations. Don't dismiss it as a debugger bug—it's showing you reality. Update your mental model.

The Phase 2 Time Investment

Phase 2 is where you spend the most time—typically 8-16 hours over 2-3 days. This seems like a lot, but compare it to the alternative:

Phase 2's 8-16 hours gives you complete, confident understanding. It's not expensive—it's efficient.

7.7.3 Phase 3: Performance Understanding (Optional)

Phase 3 answers a different question than Phases 1 and 2: "Is this code slow, and if so, why?"

Notice that word "optional" in the phase title. This is deliberate. Many developers jump straight to performance profiling before they understand what the code does. This is backwards. You must understand execution flow (Phases 1 and 2) before you can meaningfully optimize performance.

When to Skip Phase 3: If the code works acceptably and nobody is complaining about performance, skip this phase entirely. Premature optimization wastes time. Focus on understanding and correctness first.

When to Do Phase 3:

The Phase 3 Mindset Shift

In Phases 1 and 2, you asked "What runs?" In Phase 3, you ask "What's slow?" These are different questions requiring different tools.

Debuggers show you what executes but hide performance characteristics. Profilers show you performance characteristics but hide execution details. This is why Phase 3 comes after Phase 2—you need the execution understanding first to interpret profiler results.

Step 1: Profile Hot Paths Identified in Phase 2

Return to your Phase 2 notes and execution diagram. Which parts of the flow seemed complex or database-heavy? Those are your profiling targets.

For our user registration example, Phase 2 revealed:

1. Form validation (includes DB query for email uniqueness)

2. User.save() operation

3. Three signal handlers (with DB operations)

4. Celery task queuing

Let's profile the entire registration flow first to see where time goes:

# accounts/views.py

import cProfile

import pstats

from io import StringIO



def register_user(request):

    if request.method == 'POST':

        profiler = cProfile.Profile()

        profiler.enable()



        form = RegistrationForm(request.POST)

        if form.is_valid():

            user = form.save()

            login(request, user)



        profiler.disable()



        # Print profiling results to console

        stream = StringIO()

        stats = pstats.Stats(profiler, stream=stream)

        stats.strip_dirs()

        stats.sort_stats('cumulative')

        stats.print_stats(20)  # Top 20 time consumers

        print(stream.getvalue())



        if form.is_valid():

            return redirect('welcome')

    # ... rest of view

This is temporary instrumentation—you'll remove it after Phase 3. That's fine. You're not building a permanent profiling system; you're answering a specific performance question.

Run the registration flow and check your console. You'll see output like:

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)

        1    0.000    0.000    0.245    0.245 views.py:42(register_user)

        1    0.001    0.001    0.156    0.156 models.py:89(save)

       23    0.023    0.001    0.089    0.004 db/models/query.py:1247(get)

        8    0.012    0.002    0.067    0.008 db/backends/utils.py:67(execute)

        1    0.003    0.003    0.045    0.045 tasks.py:12(send_welcome_email)

        3    0.002    0.001    0.034    0.011 signals.py:18(receiver)

Read this carefully:

The Key Insight Here: models.py:89(save) took 0.156 seconds cumulative—that's 64% of the total 0.245 seconds. Within that, 23 calls to query.py:get took 0.089 seconds. This immediately suggests database query optimization is your performance target.

Step 2: Analyze Database Query Patterns

The profiler identified database operations as the bottleneck. Now use Django Debug Toolbar's SQL panel to see what queries actually run.

Perform the registration action again with Debug Toolbar active. Open the SQL panel:

SELECT "auth_user"."id", "auth_user"."email"

FROM "auth_user"

WHERE "auth_user"."email" = 'new@example.com'

[Duplicated 3 times]



SELECT "auth_user"."id", "auth_user"."username" ...

FROM "auth_user"

WHERE "auth_user"."id" = 42



SELECT "accounts_userprofile"."id", ...

FROM "accounts_userprofile"

WHERE "accounts_userprofile"."user_id" = 42



SELECT "django_session"."session_key", ...

FROM "django_session"

WHERE "django_session"."session_key" = 'abc123...'

Notice Carefully: The email uniqueness check runs THREE times. That's wasteful. Looking at the toolbar's Timeline panel, you see:

1. Form.clean() queries email (necessary)

2. Signal handler queries email (why?)

3. Another signal handler queries email (why??)

Go back to your signal handler code with this knowledge:

@receiver(post_save, sender=User)

def log_registration(sender, instance, created, **kwargs):

    if created:

        # This code checks if email is a test account

        if User.objects.filter(email=instance.email).exclude(id=instance.id).exists():

            # ... logging logic

There's your problem. The signal handler is re-querying the database for the email that already exists on the instance object. It should be:

@receiver(post_save, sender=User)

def log_registration(sender, instance, created, **kwargs):

    if created:

        # Use the instance we already have

        if instance.email.endswith('@test.com'):

            # ... logging logic

This Is The Value of Phase 3: You found a performance issue that isn't architecturally significant—you wouldn't optimize this without profiling—but it's a quick win that saves 67ms per registration.

Identifying N+1 Queries

The most common Django performance issue is N+1 queries. Here's what to look for in Debug Toolbar:

-- One query to get users

SELECT "auth_user".* FROM "auth_user"



-- Then N queries to get each user's profile (BAD!)

SELECT "accounts_userprofile".* FROM "accounts_userprofile" WHERE user_id = 1

SELECT "accounts_userprofile".* FROM "accounts_userprofile" WHERE user_id = 2

SELECT "accounts_userprofile".* FROM "accounts_userprofile" WHERE user_id = 3

... (repeated N times)

The solution is select_related() or prefetch_related():

# Instead of:

users = User.objects.all()

for user in users:

    print(user.profile.avatar)  # Causes N queries
# Use:



users = User.objects.select_related('profile').all()

for user in users:

print(user.profile.avatar) # One query with JOIN

Django Debug Toolbar highlights these automatically with orange/red warning indicators on duplicate similar queries.

Step 3: Identify Optimization Opportunities

By now you've profiled the code and analyzed the queries. Create a prioritized list of optimization opportunities:

Performance Optimization Opportunities (User Registration):



High Impact:



1. Remove duplicate email queries in signal handlers

   - Current: 3 queries, ~67ms

   - After fix: 1 query, ~22ms

   - Effort: 5 minutes



2. Add index on User.email for uniqueness checks

   - Current: Full table scan, ~15ms

   - After fix: Index lookup, ~2ms

   - Effort: One migration



Medium Impact: 3. Use select_related when querying users with profiles



- Saves 1 query per user displayed

- Only matters on user list pages



Low Impact: 4. Cache user profile avatars



- Saves disk I/O, minimal time

- Added complexity, probably not worth it

Notice This Pattern: You're prioritizing by impact and effort, not by technical elegance. The duplicate query fix takes 5 minutes and saves 45ms. The caching solution takes 2 hours and saves 3ms. Do the first one, skip the second one.

Phase 3 for Different Technology Stacks

React Performance Profiling: Use the React DevTools Profiler to record a user interaction, then analyze the flame graph:

Render took 245ms:



- App component: 245ms

  - CartPage component: 230ms

    - CartItem component (rendered 50 times): 180ms

      - PriceDisplay component (50 times): 120ms

This immediately shows you: PriceDisplay is being instantiated 50 times and taking 120ms total. That's 2.4ms per instance—normally fine, but multiplied by 50 it becomes noticeable.

Investigate PriceDisplay:

function PriceDisplay({ amount }) {
  // This recalculates on EVERY render

  const formatted = new Intl.NumberFormat("en-US", {
    style: "currency",

    currency: "USD",
  }).format(amount);

  return <span>{formatted}</span>;
}

The fix: memoize the formatting function or the component itself:

const PriceDisplay = React.memo(function PriceDisplay({ amount }) {
  const formatted = useMemo(
    () =>
      new Intl.NumberFormat("en-US", {
        style: "currency",

        currency: "USD",
      }).format(amount),

    [amount],
  );

  return <span>{formatted}</span>;
});

Node.js/Express Profiling: Use clinic for comprehensive profiling:

npm install -g clinic

clinic doctor -- node server.js

Perform the slow operation, then stop the server. Clinic generates an HTML report showing CPU usage, event loop delay, and memory patterns. Look for:

FastAPI/Async Python: Use py-spy for sampling profiling:

# Start your FastAPI server

uvicorn main:app



# In another terminal, get the process ID

ps aux | grep uvicorn



# Profile for 30 seconds

py-spy record -o profile.svg --pid <PID> -- sleep 30



# Perform the slow operation during these 30 seconds

# Then open profile.svg to see the flame graph

Async code profiling has special considerations:

The Phase 3 Time Budget

Phase 3 typically takes 4-8 hours:

Don't spend weeks optimizing. Get the low-hanging fruit (duplicate queries, missing indexes, obvious inefficiencies), document the rest, and move on. Performance optimization has diminishing returns—the first few fixes give you 80% of the gains.

When Phase 3 Reveals Architectural Problems

Sometimes profiling reveals that the architecture itself is the bottleneck:

Profile Results:

  - 2,347 database queries per page load

  - 4.5 seconds total page load time

  - 89% of time spent in ORM operations

This isn't a "fix a few queries" problem—it's an "rethink the data access layer" problem. When Phase 3 reveals this:

  1. Document the findings clearly: Show concrete numbers and examples

  2. Estimate refactoring effort: "Implementing caching layer: 2-3 weeks"

  3. Compare to business impact: "Page load time drives 30% bounce rate"

  4. Get stakeholder buy-in: This is architecture work, not a quick fix

  5. Don't attempt quick hacks: Architectural problems need architectural solutions

The value of Phase 3 is knowing whether you have a tactical problem (a few slow queries) or a strategic problem (wrong architecture). Respond accordingly.