Skip to content
academia.sh

Lesson 24 / 24

Source Debugging

Stopping a program at a chosen point and reading its state; the question each breakpoint type answers, stepping and reading scope, resolving a location in production output back to source with a source map, and deploying the map.

Contents

The tools so far measured a result: which rule won, how long the request took, which function held the main thread, which object stayed in memory. None answer “what was the variable’s value when this line ran.”

Why a row of the measurement list rendered missing, which branch a recording took under which condition — this is only seen by stopping the program at a chosen point and reading its state there. This lesson covers the pause mechanism and how paused code in production output is traced back to its source.

What Pausing Means

When a breakpoint is reached, the main thread stops. What stops is not only that function: it is the entire page. Timers do not fire, events are not processed, no frame is produced. The state read is exactly the state at the moment that instruction was about to run.

Two consequences follow. First, time-dependent behavior breaks under pausing: stopped mid-animation and resumed, the elapsed time has actually passed, and duration-based calculations jump. Timing problems are examined with a recording, not by pausing.

Second, the value read at a stopped point is more reliable than a printed one. An object written to the log is read at the moment it is expanded in the console; that is after the write, and the object may have changed in between. A breakpoint removes this uncertainty.

Breakpoint Types and the Question They Answer

The breakpoint type is chosen according to the question being asked.

Line breakpoint answers “was this line reached, and what was the state then.” It is the most basic type and is used when the line’s code is already known.

Conditional breakpoint stops on the same line only when a condition holds. It is needed to stop only on the one row that renders missing in a hundred-row list; an unconditional breakpoint here stops a hundred times and becomes unusable.

Logpoint does not stop, it only writes an expression’s value to the log. It is a way to add a log line without changing the code, and so eliminates the temporary print lines that would otherwise leak into version control.

Exception breakpoint answers “where was this error thrown from” and has two modes: stop on every exception, or only on uncaught ones. The first mode finds places where an error is caught and swallowed.

Subtree modification breakpoint answers “who changed this node.” It stops on attribute change, on child addition or removal, or on removal of the node itself. It is the shortest route to finding who is responsible for an element that unexpectedly disappears or changes; it requires no idea of which part of the code to search.

Event breakpoint stops when a given event type begins being handled and answers “which listener is handling this click.” In the structure established in the Event Delegation lesson, a listener sitting on an ancestor far from the target makes this tool necessary.

Request breakpoint stops when a network request is made whose address matches a given pattern, and shows the call chain that started the request.

Stepping, Scope, and Call Stack

In a paused program, three views are read together.

The call stack says how execution got to that point. Every frame can be selected, and the selected frame’s scope is read; where a bad value came from is found by walking up the stack. Asynchronous boundaries are added to the stack too: the relationship between code running after a promise and the call that started the promise is shown as a separate section. Without this, the stack starts at the event loop and says nothing.

The scope view lists local variables in the selected frame, values captured in a closure, and the global scope separately. The closure section is especially valuable in diagnosis: a variable being different from what is expected mostly comes from how a loop variable was captured.

Stepping is done in three modes. Step over runs the call on the current line and stops on the next line. Step into stops at the first line of the called function. Step out runs until the current function returns and goes back to the caller.

Stepping’s practical problem is falling into helper code: trying to step into an event listener, intervening infrastructure frames scatter the stepping. Tools allow specific files to be ignored for stepping; ignored files’ frames are hidden in the stack, and step-into skips them. Staying in your own code is achieved with this setting.

Stopping in Production Output: Source Maps

The deployed script is not the written script: names are shortened, lines merged, perhaps reduced to a different syntax. A program paused in this output shows a single unreadable line.

The structure defined in the Source Maps lesson of the Tooling, Modules, and Ecosystem course closes this gap: a mapping table tying every position in the generated file to a position in the original file. The table is encoded with diffs and variable-length quantities to save space. Decoding it is a computable operation.

// sourcemap.mjs — VLQ encoding/decoding and resolving a generated position to its source
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

function vlqEncode(number) {
  let value = number < 0 ? (-number << 1) | 1 : number << 1; // sign in the lowest bit
  let output = "";
  do {
    let chunk = value & 0b11111;
    value >>>= 5;
    if (value > 0) chunk |= 0b100000;                  // continuation bit
    output += ALPHABET[chunk];
  } while (value > 0);
  return output;
}

function vlqDecode(text) {
  const numbers = [];
  let value = 0, shift = 0;
  for (const char of text) {
    const chunk = ALPHABET.indexOf(char);
    value += (chunk & 0b11111) << shift;
    if (chunk & 0b100000) { shift += 5; continue; } // continuation bit: move to the next char
    numbers.push(value & 1 ? -(value >>> 1) : value >>> 1);
    value = 0; shift = 0;
  }
  return numbers;
}

// Absolute mappings: position in the generated file → position in the source
const MAPPINGS = [
  [ // generated line 0
    { generatedColumn: 0, source: 0, line: 0, column: 0, name: 0 },
    { generatedColumn: 12, source: 0, line: 4, column: 2, name: 1 },
    { generatedColumn: 31, source: 0, line: 5, column: 14, name: null },
  ],
  [ // generated line 1
    { generatedColumn: 0, source: 0, line: 9, column: 0, name: null },
    { generatedColumn: 8, source: 0, line: 11, column: 4, name: null },
  ],
];

// Turns absolute mappings into a difference sequence and encodes it: fields are stored as diffs.
function generateMappings(mappings) {
  let prevSource = 0, prevLine = 0, prevColumn = 0, prevName = 0;
  return mappings.map((lineMappings) => {
    let prevGeneratedColumn = 0;
    return lineMappings.map((e) => {
      const fields = [e.generatedColumn - prevGeneratedColumn, e.source - prevSource,
        e.line - prevLine, e.column - prevColumn];
      if (e.name !== null) fields.push(e.name - prevName);
      prevGeneratedColumn = e.generatedColumn;
      prevSource = e.source; prevLine = e.line; prevColumn = e.column;
      if (e.name !== null) prevName = e.name;
      return fields.map(vlqEncode).join("");
    }).join(",");
  }).join(";");
}

const map = {
  version: 3,
  file: "measurements.min.js",
  sources: ["measurements.js"],
  names: ["drawMeasurements", "buildRow"],
  mappings: generateMappings(MAPPINGS),
};
console.log("mappings:", map.mappings);

// Decoding: reconstructs absolute positions from the difference sequence.
function parseMappings(mappings) {
  let source = 0, line = 0, column = 0, name = 0;
  return mappings.split(";").map((lineText) => {
    let generatedColumn = 0;
    if (lineText === "") return [];
    return lineText.split(",").map((segment) => {
      const a = vlqDecode(segment);
      generatedColumn += a[0]; source += a[1]; line += a[2]; column += a[3];
      if (a.length === 5) name += a[4];
      return { generatedColumn, source, line, column, name: a.length === 5 ? name : null };
    });
  });
}

const parsed = parseMappings(map.mappings);

// From generated position to source: take the LAST mapping in the line not past the column.
function findInSource(generatedLine, generatedColumn) {
  const lineMappings = parsed[generatedLine] ?? [];
  let found = null;
  for (const e of lineMappings) if (e.generatedColumn <= generatedColumn) found = e;
  if (!found) return null;
  return {
    file: map.sources[found.source],
    line: found.line + 1,
    column: found.column + 1,
    name: found.name === null ? null : map.names[found.name],
  };
}

console.log("\ngenerated position → position in source");
for (const [s, k] of [[0, 0], [0, 20], [0, 40], [1, 3], [1, 12]]) {
  const c = findInSource(s, k);
  console.log(`  measurements.min.js ${s + 1}:${k + 1}`.padEnd(28) +
    `→ ${c.file} ${c.line}:${c.column}` + (c.name ? `  (${c.name})` : ""));
}
mappings: AAAAA,YAIEC,mBACY;AAId,QAEI

generated position → position in source
  measurements.min.js 1:1   → measurements.js 1:1  (drawMeasurements)
  measurements.min.js 1:21  → measurements.js 5:3  (buildRow)
  measurements.min.js 1:41  → measurements.js 6:15
  measurements.min.js 2:4   → measurements.js 10:1
  measurements.min.js 2:13  → measurements.js 12:5

Three details in the output are worth noticing.

First, the mapping is not complete. There is no entry for the twenty-first column; resolution falls back to the last mapping not past that column. This is why a breakpoint in the debugger drifts a little from the line it was written on: it holds onto the nearest mapping.

Second, the name field is optional. The third mapping has none; a shortened variable’s original name can only be recovered if its mapping carries a name. This is why single-letter names keep showing up in the scope view.

Third, the encoding is diff-based. Decoding a segment depends on the ones before it; reading cannot start from the middle of the table. This keeps the file small and forces a full decode.

Deploying the Map

A source map is a separate file, tied to the generated file with a comment line at its end. The deployment decision comes down to three options.

Serving the map publicly is easiest and makes the source readable. For a closed-source application, this means publishing the code.

Generating the map but not publishing it is the common middle path: the map is uploaded to an error-tracking system, not served from the server. The user’s browser keeps the shortened stack trace; the developer side sees it resolved.

Never generating it produces no measurable gain — the map is only downloaded when requested — and makes diagnosing production errors impossible.

Whether the map carries the source text inside itself is a separate decision too: if it does, the file grows but the original files need no separate access; if not, the map stays small and the sources need to be served separately.

Summary

  • A breakpoint stops the main thread; time-dependent behavior breaks under pausing, so timing problems are examined with a recording instead.
  • Breakpoint type is chosen by the question asked: line, condition, log, exception, subtree change, event, and request.
  • The call stack says how execution reached that point, the scope view gives values in the selected frame; asynchronous boundaries are added to the stack separately.
  • Stepping has three modes; ignoring helper files keeps stepping in your own code.
  • A source map is a diff-encoded mapping table; resolution falls to the nearest mapping, and the name field is optional.
  • Map deployment is a decision between code readability and the diagnosability of production errors.

Course Wrap-Up

This course treated the browser as a working environment and built it in four layers.

The Document and Events layer turned the page into a data structure that can be read and modified from inside a program; the event’s defined path from root to target and back made it possible to manage many elements with a single listener, intervene in the browser’s default behavior, and build keyboard and form interaction.

The Browser APIs layer added persistence, navigation control, observation ability, frame-synced updates, localized formatting, and the ability to offload the main thread to the page.

The Web Components and Offline layer established two independences: an interface piece’s independence from the page — with its own name, lifecycle, encapsulated structure, and style — and the application’s independence from the network — with an intervening worker, per-resource cache decisions, and an installable shell.

The Developer Tools layer put diagnosis on top of these three. Every tool’s question was separate: which rule won, where and how long a request was served from, where the main thread got stuck, which object was never released, what the value was at that line. The common method was the same each time — first determine the question, then read the measure that answers it.

Everything this course built was done by hand: creating a node, binding a listener, writing a change to the relevant element, undoing all of it when tearing down. The measurement badge became a component, but the fifteen badges on the dashboard staying in sync with the list still depends on hand-written update code. The code itself computes which nodes should change when a record is added to the list; as this computation grows, it turns into a source of bugs.

The next course, M14/K05 · Component-Based Interface Development, reverses this computation. The interface is defined not by how to change it step by step, but by what it should be given the current state; finding the difference and applying it to the tree becomes the infrastructure’s job. With platform capabilities and diagnostic tools now in place, the turn is to the declarative interface model and the composition of components.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close